|
1
|
|
|
/*! |
|
2
|
|
|
* FullCalendar v2.1.1 |
|
3
|
|
|
* Docs & License: http://arshaw.com/fullcalendar/ |
|
4
|
|
|
* (c) 2013 Adam Shaw |
|
5
|
|
|
*/ |
|
6
|
|
|
|
|
7
|
|
|
(function(factory) { |
|
8
|
|
|
if (typeof define === 'function' && define.amd) { |
|
9
|
|
|
define([ 'jquery', 'moment' ], factory); |
|
10
|
|
|
} |
|
11
|
|
|
else { |
|
12
|
|
|
factory(jQuery, moment); |
|
13
|
|
|
} |
|
14
|
|
|
})(function($, moment) { |
|
15
|
|
|
|
|
16
|
|
|
;; |
|
17
|
|
|
|
|
18
|
|
|
var defaults = { |
|
19
|
|
|
|
|
20
|
|
|
lang: 'en', |
|
21
|
|
|
|
|
22
|
|
|
defaultTimedEventDuration: '02:00:00', |
|
23
|
|
|
defaultAllDayEventDuration: { days: 1 }, |
|
24
|
|
|
forceEventDuration: false, |
|
25
|
|
|
nextDayThreshold: '09:00:00', // 9am |
|
26
|
|
|
|
|
27
|
|
|
// display |
|
28
|
|
|
defaultView: 'month', |
|
29
|
|
|
aspectRatio: 1.35, |
|
30
|
|
|
header: { |
|
31
|
|
|
left: 'title', |
|
32
|
|
|
center: '', |
|
33
|
|
|
right: 'today prev,next' |
|
34
|
|
|
}, |
|
35
|
|
|
weekends: true, |
|
36
|
|
|
weekNumbers: false, |
|
37
|
|
|
|
|
38
|
|
|
weekNumberTitle: 'W', |
|
39
|
|
|
weekNumberCalculation: 'local', |
|
40
|
|
|
|
|
41
|
|
|
//editable: false, |
|
42
|
|
|
|
|
43
|
|
|
// event ajax |
|
44
|
|
|
lazyFetching: true, |
|
45
|
|
|
startParam: 'start', |
|
46
|
|
|
endParam: 'end', |
|
47
|
|
|
timezoneParam: 'timezone', |
|
48
|
|
|
|
|
49
|
|
|
timezone: false, |
|
50
|
|
|
|
|
51
|
|
|
//allDayDefault: undefined, |
|
52
|
|
|
|
|
53
|
|
|
// time formats |
|
54
|
|
|
titleFormat: { |
|
55
|
|
|
month: 'MMMM YYYY', // like "September 1986". each language will override this |
|
56
|
|
|
week: 'll', // like "Sep 4 1986" |
|
57
|
|
|
day: 'LL' // like "September 4 1986" |
|
58
|
|
|
}, |
|
59
|
|
|
columnFormat: { |
|
60
|
|
|
month: 'ddd', // like "Sat" |
|
61
|
|
|
week: generateWeekColumnFormat, |
|
62
|
|
|
day: 'dddd' // like "Saturday" |
|
63
|
|
|
}, |
|
64
|
|
|
timeFormat: { // for event elements |
|
65
|
|
|
'default': generateShortTimeFormat |
|
66
|
|
|
}, |
|
67
|
|
|
|
|
68
|
|
|
displayEventEnd: { |
|
69
|
|
|
month: false, |
|
70
|
|
|
basicWeek: false, |
|
71
|
|
|
'default': true |
|
72
|
|
|
}, |
|
73
|
|
|
|
|
74
|
|
|
// locale |
|
75
|
|
|
isRTL: false, |
|
76
|
|
|
defaultButtonText: { |
|
77
|
|
|
prev: "prev", |
|
78
|
|
|
next: "next", |
|
79
|
|
|
prevYear: "prev year", |
|
80
|
|
|
nextYear: "next year", |
|
81
|
|
|
today: 'today', |
|
82
|
|
|
month: 'month', |
|
83
|
|
|
week: 'week', |
|
84
|
|
|
day: 'day' |
|
85
|
|
|
}, |
|
86
|
|
|
|
|
87
|
|
|
buttonIcons: { |
|
88
|
|
|
prev: 'left-single-arrow', |
|
89
|
|
|
next: 'right-single-arrow', |
|
90
|
|
|
prevYear: 'left-double-arrow', |
|
91
|
|
|
nextYear: 'right-double-arrow' |
|
92
|
|
|
}, |
|
93
|
|
|
|
|
94
|
|
|
// jquery-ui theming |
|
95
|
|
|
theme: false, |
|
96
|
|
|
themeButtonIcons: { |
|
97
|
|
|
prev: 'circle-triangle-w', |
|
98
|
|
|
next: 'circle-triangle-e', |
|
99
|
|
|
prevYear: 'seek-prev', |
|
100
|
|
|
nextYear: 'seek-next' |
|
101
|
|
|
}, |
|
102
|
|
|
|
|
103
|
|
|
dragOpacity: .75, |
|
104
|
|
|
dragRevertDuration: 500, |
|
105
|
|
|
dragScroll: true, |
|
106
|
|
|
|
|
107
|
|
|
//selectable: false, |
|
108
|
|
|
unselectAuto: true, |
|
109
|
|
|
|
|
110
|
|
|
dropAccept: '*', |
|
111
|
|
|
|
|
112
|
|
|
eventLimit: false, |
|
113
|
|
|
eventLimitText: 'more', |
|
114
|
|
|
eventLimitClick: 'popover', |
|
115
|
|
|
dayPopoverFormat: 'LL', |
|
116
|
|
|
|
|
117
|
|
|
handleWindowResize: true, |
|
118
|
|
|
windowResizeDelay: 200 // milliseconds before a rerender happens |
|
119
|
|
|
|
|
120
|
|
|
}; |
|
121
|
|
|
|
|
122
|
|
|
|
|
123
|
|
|
function generateShortTimeFormat(options, langData) { |
|
124
|
|
|
return langData.longDateFormat('LT') |
|
125
|
|
|
.replace(':mm', '(:mm)') |
|
126
|
|
|
.replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs |
|
127
|
|
|
.replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand |
|
128
|
|
|
} |
|
129
|
|
|
|
|
130
|
|
|
|
|
131
|
|
|
function generateWeekColumnFormat(options, langData) { |
|
132
|
|
|
var format = langData.longDateFormat('L'); // for the format like "MM/DD/YYYY" |
|
133
|
|
|
format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, ''); // strip the year off the edge, as well as other misc non-whitespace chars |
|
134
|
|
|
if (options.isRTL) { |
|
135
|
|
|
format += ' ddd'; // for RTL, add day-of-week to end |
|
136
|
|
|
} |
|
137
|
|
|
else { |
|
138
|
|
|
format = 'ddd ' + format; // for LTR, add day-of-week to beginning |
|
139
|
|
|
} |
|
140
|
|
|
return format; |
|
141
|
|
|
} |
|
142
|
|
|
|
|
143
|
|
|
|
|
144
|
|
|
var langOptionHash = { |
|
145
|
|
|
en: { |
|
146
|
|
|
columnFormat: { |
|
147
|
|
|
week: 'ddd M/D' // override for english. different from the generated default, which is MM/DD |
|
148
|
|
|
}, |
|
149
|
|
|
dayPopoverFormat: 'dddd, MMMM D' |
|
150
|
|
|
} |
|
151
|
|
|
}; |
|
152
|
|
|
|
|
153
|
|
|
|
|
154
|
|
|
// right-to-left defaults |
|
155
|
|
|
var rtlDefaults = { |
|
156
|
|
|
header: { |
|
157
|
|
|
left: 'next,prev today', |
|
158
|
|
|
center: '', |
|
159
|
|
|
right: 'title' |
|
160
|
|
|
}, |
|
161
|
|
|
buttonIcons: { |
|
162
|
|
|
prev: 'right-single-arrow', |
|
163
|
|
|
next: 'left-single-arrow', |
|
164
|
|
|
prevYear: 'right-double-arrow', |
|
165
|
|
|
nextYear: 'left-double-arrow' |
|
166
|
|
|
}, |
|
167
|
|
|
themeButtonIcons: { |
|
168
|
|
|
prev: 'circle-triangle-e', |
|
169
|
|
|
next: 'circle-triangle-w', |
|
170
|
|
|
nextYear: 'seek-prev', |
|
171
|
|
|
prevYear: 'seek-next' |
|
172
|
|
|
} |
|
173
|
|
|
}; |
|
174
|
|
|
|
|
175
|
|
|
;; |
|
176
|
|
|
|
|
177
|
|
|
var fc = $.fullCalendar = { version: "2.1.1" }; |
|
178
|
|
|
var fcViews = fc.views = {}; |
|
179
|
|
|
|
|
180
|
|
|
|
|
181
|
|
|
$.fn.fullCalendar = function(options) { |
|
182
|
|
|
var args = Array.prototype.slice.call(arguments, 1); // for a possible method call |
|
183
|
|
|
var res = this; // what this function will return (this jQuery object by default) |
|
184
|
|
|
|
|
185
|
|
|
this.each(function(i, _element) { // loop each DOM element involved |
|
186
|
|
|
var element = $(_element); |
|
187
|
|
|
var calendar = element.data('fullCalendar'); // get the existing calendar object (if any) |
|
188
|
|
|
var singleRes; // the returned value of this single method call |
|
189
|
|
|
|
|
190
|
|
|
// a method call |
|
191
|
|
|
if (typeof options === 'string') { |
|
192
|
|
|
if (calendar && $.isFunction(calendar[options])) { |
|
193
|
|
|
singleRes = calendar[options].apply(calendar, args); |
|
194
|
|
|
if (!i) { |
|
195
|
|
|
res = singleRes; // record the first method call result |
|
196
|
|
|
} |
|
197
|
|
|
if (options === 'destroy') { // for the destroy method, must remove Calendar object data |
|
198
|
|
|
element.removeData('fullCalendar'); |
|
199
|
|
|
} |
|
200
|
|
|
} |
|
201
|
|
|
} |
|
202
|
|
|
// a new calendar initialization |
|
203
|
|
|
else if (!calendar) { // don't initialize twice |
|
204
|
|
|
calendar = new Calendar(element, options); |
|
205
|
|
|
element.data('fullCalendar', calendar); |
|
206
|
|
|
calendar.render(); |
|
207
|
|
|
} |
|
208
|
|
|
}); |
|
209
|
|
|
|
|
210
|
|
|
return res; |
|
211
|
|
|
}; |
|
212
|
|
|
|
|
213
|
|
|
|
|
214
|
|
|
// function for adding/overriding defaults |
|
215
|
|
|
function setDefaults(d) { |
|
216
|
|
|
mergeOptions(defaults, d); |
|
217
|
|
|
} |
|
218
|
|
|
|
|
219
|
|
|
|
|
220
|
|
|
// Recursively combines option hash-objects. |
|
221
|
|
|
// Better than `$.extend(true, ...)` because arrays are not traversed/copied. |
|
222
|
|
|
// |
|
223
|
|
|
// called like: |
|
224
|
|
|
// mergeOptions(target, obj1, obj2, ...) |
|
225
|
|
|
// |
|
226
|
|
|
function mergeOptions(target) { |
|
227
|
|
|
|
|
228
|
|
|
function mergeIntoTarget(name, value) { |
|
229
|
|
|
if ($.isPlainObject(value) && $.isPlainObject(target[name]) && !isForcedAtomicOption(name)) { |
|
230
|
|
|
// merge into a new object to avoid destruction |
|
231
|
|
|
target[name] = mergeOptions({}, target[name], value); // combine. `value` object takes precedence |
|
232
|
|
|
} |
|
233
|
|
|
else if (value !== undefined) { // only use values that are set and not undefined |
|
234
|
|
|
target[name] = value; |
|
235
|
|
|
} |
|
236
|
|
|
} |
|
237
|
|
|
|
|
238
|
|
|
for (var i=1; i<arguments.length; i++) { |
|
239
|
|
|
$.each(arguments[i], mergeIntoTarget); |
|
240
|
|
|
} |
|
241
|
|
|
|
|
242
|
|
|
return target; |
|
243
|
|
|
} |
|
244
|
|
|
|
|
245
|
|
|
|
|
246
|
|
|
// overcome sucky view-option-hash and option-merging behavior messing with options it shouldn't |
|
247
|
|
|
function isForcedAtomicOption(name) { |
|
248
|
|
|
// Any option that ends in "Time" or "Duration" is probably a Duration, |
|
249
|
|
|
// and these will commonly be specified as plain objects, which we don't want to mess up. |
|
250
|
|
|
return /(Time|Duration)$/.test(name); |
|
251
|
|
|
} |
|
252
|
|
|
// FIX: find a different solution for view-option-hashes and have a whitelist |
|
253
|
|
|
// for options that can be recursively merged. |
|
254
|
|
|
|
|
255
|
|
|
;; |
|
256
|
|
|
|
|
257
|
|
|
//var langOptionHash = {}; // initialized in defaults.js |
|
258
|
|
|
fc.langs = langOptionHash; // expose |
|
259
|
|
|
|
|
260
|
|
|
|
|
261
|
|
|
// Initialize jQuery UI Datepicker translations while using some of the translations |
|
262
|
|
|
// for our own purposes. Will set this as the default language for datepicker. |
|
263
|
|
|
// Called from a translation file. |
|
264
|
|
|
fc.datepickerLang = function(langCode, datepickerLangCode, options) { |
|
265
|
|
|
var langOptions = langOptionHash[langCode]; |
|
266
|
|
|
|
|
267
|
|
|
// initialize FullCalendar's lang hash for this language |
|
268
|
|
|
if (!langOptions) { |
|
269
|
|
|
langOptions = langOptionHash[langCode] = {}; |
|
270
|
|
|
} |
|
271
|
|
|
|
|
272
|
|
|
// merge certain Datepicker options into FullCalendar's options |
|
273
|
|
|
mergeOptions(langOptions, { |
|
274
|
|
|
isRTL: options.isRTL, |
|
275
|
|
|
weekNumberTitle: options.weekHeader, |
|
276
|
|
|
titleFormat: { |
|
277
|
|
|
month: options.showMonthAfterYear ? |
|
278
|
|
|
'YYYY[' + options.yearSuffix + '] MMMM' : |
|
279
|
|
|
'MMMM YYYY[' + options.yearSuffix + ']' |
|
280
|
|
|
}, |
|
281
|
|
|
defaultButtonText: { |
|
282
|
|
|
// the translations sometimes wrongly contain HTML entities |
|
283
|
|
|
prev: stripHtmlEntities(options.prevText), |
|
284
|
|
|
next: stripHtmlEntities(options.nextText), |
|
285
|
|
|
today: stripHtmlEntities(options.currentText) |
|
286
|
|
|
} |
|
287
|
|
|
}); |
|
288
|
|
|
|
|
289
|
|
|
// is jQuery UI Datepicker is on the page? |
|
290
|
|
|
if ($.datepicker) { |
|
291
|
|
|
|
|
292
|
|
|
// Register the language data. |
|
293
|
|
|
// FullCalendar and MomentJS use language codes like "pt-br" but Datepicker |
|
294
|
|
|
// does it like "pt-BR" or if it doesn't have the language, maybe just "pt". |
|
295
|
|
|
// Make an alias so the language can be referenced either way. |
|
296
|
|
|
$.datepicker.regional[datepickerLangCode] = |
|
297
|
|
|
$.datepicker.regional[langCode] = // alias |
|
298
|
|
|
options; |
|
299
|
|
|
|
|
300
|
|
|
// Alias 'en' to the default language data. Do this every time. |
|
301
|
|
|
$.datepicker.regional.en = $.datepicker.regional['']; |
|
302
|
|
|
|
|
303
|
|
|
// Set as Datepicker's global defaults. |
|
304
|
|
|
$.datepicker.setDefaults(options); |
|
305
|
|
|
} |
|
306
|
|
|
}; |
|
307
|
|
|
|
|
308
|
|
|
|
|
309
|
|
|
// Sets FullCalendar-specific translations. Also sets the language as the global default. |
|
310
|
|
|
// Called from a translation file. |
|
311
|
|
|
fc.lang = function(langCode, options) { |
|
312
|
|
|
var langOptions; |
|
313
|
|
|
|
|
314
|
|
|
if (options) { |
|
315
|
|
|
langOptions = langOptionHash[langCode]; |
|
316
|
|
|
|
|
317
|
|
|
// initialize the hash for this language |
|
318
|
|
|
if (!langOptions) { |
|
319
|
|
|
langOptions = langOptionHash[langCode] = {}; |
|
320
|
|
|
} |
|
321
|
|
|
|
|
322
|
|
|
mergeOptions(langOptions, options || {}); |
|
323
|
|
|
} |
|
324
|
|
|
|
|
325
|
|
|
// set it as the default language for FullCalendar |
|
326
|
|
|
defaults.lang = langCode; |
|
327
|
|
|
}; |
|
328
|
|
|
;; |
|
329
|
|
|
|
|
330
|
|
|
|
|
331
|
|
|
function Calendar(element, instanceOptions) { |
|
332
|
|
|
var t = this; |
|
333
|
|
|
|
|
334
|
|
|
|
|
335
|
|
|
|
|
336
|
|
|
// Build options object |
|
337
|
|
|
// ----------------------------------------------------------------------------------- |
|
338
|
|
|
// Precedence (lowest to highest): defaults, rtlDefaults, langOptions, instanceOptions |
|
339
|
|
|
|
|
340
|
|
|
instanceOptions = instanceOptions || {}; |
|
341
|
|
|
|
|
342
|
|
|
var options = mergeOptions({}, defaults, instanceOptions); |
|
343
|
|
|
var langOptions; |
|
344
|
|
|
|
|
345
|
|
|
// determine language options |
|
346
|
|
|
if (options.lang in langOptionHash) { |
|
347
|
|
|
langOptions = langOptionHash[options.lang]; |
|
348
|
|
|
} |
|
349
|
|
|
else { |
|
350
|
|
|
langOptions = langOptionHash[defaults.lang]; |
|
351
|
|
|
} |
|
352
|
|
|
|
|
353
|
|
|
if (langOptions) { // if language options exist, rebuild... |
|
354
|
|
|
options = mergeOptions({}, defaults, langOptions, instanceOptions); |
|
355
|
|
|
} |
|
356
|
|
|
|
|
357
|
|
|
if (options.isRTL) { // is isRTL, rebuild... |
|
358
|
|
|
options = mergeOptions({}, defaults, rtlDefaults, langOptions || {}, instanceOptions); |
|
359
|
|
|
} |
|
360
|
|
|
|
|
361
|
|
|
|
|
362
|
|
|
|
|
363
|
|
|
// Exports |
|
364
|
|
|
// ----------------------------------------------------------------------------------- |
|
365
|
|
|
|
|
366
|
|
|
t.options = options; |
|
367
|
|
|
t.render = render; |
|
368
|
|
|
t.destroy = destroy; |
|
369
|
|
|
t.refetchEvents = refetchEvents; |
|
370
|
|
|
t.reportEvents = reportEvents; |
|
371
|
|
|
t.reportEventChange = reportEventChange; |
|
372
|
|
|
t.rerenderEvents = renderEvents; // `renderEvents` serves as a rerender. an API method |
|
373
|
|
|
t.changeView = changeView; |
|
374
|
|
|
t.select = select; |
|
375
|
|
|
t.unselect = unselect; |
|
376
|
|
|
t.prev = prev; |
|
377
|
|
|
t.next = next; |
|
378
|
|
|
t.prevYear = prevYear; |
|
379
|
|
|
t.nextYear = nextYear; |
|
380
|
|
|
t.today = today; |
|
381
|
|
|
t.gotoDate = gotoDate; |
|
382
|
|
|
t.incrementDate = incrementDate; |
|
383
|
|
|
t.zoomTo = zoomTo; |
|
384
|
|
|
t.getDate = getDate; |
|
385
|
|
|
t.getCalendar = getCalendar; |
|
386
|
|
|
t.getView = getView; |
|
387
|
|
|
t.option = option; |
|
388
|
|
|
t.trigger = trigger; |
|
389
|
|
|
|
|
390
|
|
|
|
|
391
|
|
|
|
|
392
|
|
|
// Language-data Internals |
|
393
|
|
|
// ----------------------------------------------------------------------------------- |
|
394
|
|
|
// Apply overrides to the current language's data |
|
395
|
|
|
|
|
396
|
|
|
|
|
397
|
|
|
// Returns moment's internal locale data. If doesn't exist, returns English. |
|
398
|
|
|
// Works with moment-pre-2.8 |
|
399
|
|
|
function getLocaleData(langCode) { |
|
400
|
|
|
var f = moment.localeData || moment.langData; |
|
401
|
|
|
return f.call(moment, langCode) || |
|
402
|
|
|
f.call(moment, 'en'); // the newer localData could return null, so fall back to en |
|
403
|
|
|
} |
|
404
|
|
|
|
|
405
|
|
|
|
|
406
|
|
|
var localeData = createObject(getLocaleData(options.lang)); // make a cheap copy |
|
407
|
|
|
|
|
408
|
|
|
if (options.monthNames) { |
|
409
|
|
|
localeData._months = options.monthNames; |
|
410
|
|
|
} |
|
411
|
|
|
if (options.monthNamesShort) { |
|
412
|
|
|
localeData._monthsShort = options.monthNamesShort; |
|
413
|
|
|
} |
|
414
|
|
|
if (options.dayNames) { |
|
415
|
|
|
localeData._weekdays = options.dayNames; |
|
416
|
|
|
} |
|
417
|
|
|
if (options.dayNamesShort) { |
|
418
|
|
|
localeData._weekdaysShort = options.dayNamesShort; |
|
419
|
|
|
} |
|
420
|
|
|
if (options.firstDay != null) { |
|
421
|
|
|
var _week = createObject(localeData._week); // _week: { dow: # } |
|
422
|
|
|
_week.dow = options.firstDay; |
|
423
|
|
|
localeData._week = _week; |
|
424
|
|
|
} |
|
425
|
|
|
|
|
426
|
|
|
|
|
427
|
|
|
|
|
428
|
|
|
// Calendar-specific Date Utilities |
|
429
|
|
|
// ----------------------------------------------------------------------------------- |
|
430
|
|
|
|
|
431
|
|
|
|
|
432
|
|
|
t.defaultAllDayEventDuration = moment.duration(options.defaultAllDayEventDuration); |
|
433
|
|
|
t.defaultTimedEventDuration = moment.duration(options.defaultTimedEventDuration); |
|
434
|
|
|
|
|
435
|
|
|
|
|
436
|
|
|
// Builds a moment using the settings of the current calendar: timezone and language. |
|
437
|
|
|
// Accepts anything the vanilla moment() constructor accepts. |
|
438
|
|
|
t.moment = function() { |
|
439
|
|
|
var mom; |
|
440
|
|
|
|
|
441
|
|
|
if (options.timezone === 'local') { |
|
442
|
|
|
mom = fc.moment.apply(null, arguments); |
|
443
|
|
|
|
|
444
|
|
|
// Force the moment to be local, because fc.moment doesn't guarantee it. |
|
445
|
|
|
if (mom.hasTime()) { // don't give ambiguously-timed moments a local zone |
|
446
|
|
|
mom.local(); |
|
447
|
|
|
} |
|
448
|
|
|
} |
|
449
|
|
|
else if (options.timezone === 'UTC') { |
|
450
|
|
|
mom = fc.moment.utc.apply(null, arguments); // process as UTC |
|
451
|
|
|
} |
|
452
|
|
|
else { |
|
453
|
|
|
mom = fc.moment.parseZone.apply(null, arguments); // let the input decide the zone |
|
454
|
|
|
} |
|
455
|
|
|
|
|
456
|
|
|
if ('_locale' in mom) { // moment 2.8 and above |
|
457
|
|
|
mom._locale = localeData; |
|
458
|
|
|
} |
|
459
|
|
|
else { // pre-moment-2.8 |
|
460
|
|
|
mom._lang = localeData; |
|
461
|
|
|
} |
|
462
|
|
|
|
|
463
|
|
|
return mom; |
|
464
|
|
|
}; |
|
465
|
|
|
|
|
466
|
|
|
|
|
467
|
|
|
// Returns a boolean about whether or not the calendar knows how to calculate |
|
468
|
|
|
// the timezone offset of arbitrary dates in the current timezone. |
|
469
|
|
|
t.getIsAmbigTimezone = function() { |
|
470
|
|
|
return options.timezone !== 'local' && options.timezone !== 'UTC'; |
|
471
|
|
|
}; |
|
472
|
|
|
|
|
473
|
|
|
|
|
474
|
|
|
// Returns a copy of the given date in the current timezone of it is ambiguously zoned. |
|
475
|
|
|
// This will also give the date an unambiguous time. |
|
476
|
|
|
t.rezoneDate = function(date) { |
|
477
|
|
|
return t.moment(date.toArray()); |
|
478
|
|
|
}; |
|
479
|
|
|
|
|
480
|
|
|
|
|
481
|
|
|
// Returns a moment for the current date, as defined by the client's computer, |
|
482
|
|
|
// or overridden by the `now` option. |
|
483
|
|
|
t.getNow = function() { |
|
484
|
|
|
var now = options.now; |
|
485
|
|
|
if (typeof now === 'function') { |
|
486
|
|
|
now = now(); |
|
487
|
|
|
} |
|
488
|
|
|
return t.moment(now); |
|
489
|
|
|
}; |
|
490
|
|
|
|
|
491
|
|
|
|
|
492
|
|
|
// Calculates the week number for a moment according to the calendar's |
|
493
|
|
|
// `weekNumberCalculation` setting. |
|
494
|
|
|
t.calculateWeekNumber = function(mom) { |
|
495
|
|
|
var calc = options.weekNumberCalculation; |
|
496
|
|
|
|
|
497
|
|
|
if (typeof calc === 'function') { |
|
498
|
|
|
return calc(mom); |
|
499
|
|
|
} |
|
500
|
|
|
else if (calc === 'local') { |
|
501
|
|
|
return mom.week(); |
|
502
|
|
|
} |
|
503
|
|
|
else if (calc.toUpperCase() === 'ISO') { |
|
504
|
|
|
return mom.isoWeek(); |
|
505
|
|
|
} |
|
506
|
|
|
}; |
|
507
|
|
|
|
|
508
|
|
|
|
|
509
|
|
|
// Get an event's normalized end date. If not present, calculate it from the defaults. |
|
510
|
|
|
t.getEventEnd = function(event) { |
|
511
|
|
|
if (event.end) { |
|
512
|
|
|
return event.end.clone(); |
|
513
|
|
|
} |
|
514
|
|
|
else { |
|
515
|
|
|
return t.getDefaultEventEnd(event.allDay, event.start); |
|
516
|
|
|
} |
|
517
|
|
|
}; |
|
518
|
|
|
|
|
519
|
|
|
|
|
520
|
|
|
// Given an event's allDay status and start date, return swhat its fallback end date should be. |
|
521
|
|
|
t.getDefaultEventEnd = function(allDay, start) { // TODO: rename to computeDefaultEventEnd |
|
522
|
|
|
var end = start.clone(); |
|
523
|
|
|
|
|
524
|
|
|
if (allDay) { |
|
525
|
|
|
end.stripTime().add(t.defaultAllDayEventDuration); |
|
526
|
|
|
} |
|
527
|
|
|
else { |
|
528
|
|
|
end.add(t.defaultTimedEventDuration); |
|
529
|
|
|
} |
|
530
|
|
|
|
|
531
|
|
|
if (t.getIsAmbigTimezone()) { |
|
532
|
|
|
end.stripZone(); // we don't know what the tzo should be |
|
533
|
|
|
} |
|
534
|
|
|
|
|
535
|
|
|
return end; |
|
536
|
|
|
}; |
|
537
|
|
|
|
|
538
|
|
|
|
|
539
|
|
|
|
|
540
|
|
|
// Date-formatting Utilities |
|
541
|
|
|
// ----------------------------------------------------------------------------------- |
|
542
|
|
|
|
|
543
|
|
|
|
|
544
|
|
|
// Like the vanilla formatRange, but with calendar-specific settings applied. |
|
545
|
|
|
t.formatRange = function(m1, m2, formatStr) { |
|
546
|
|
|
|
|
547
|
|
|
// a function that returns a formatStr // TODO: in future, precompute this |
|
548
|
|
|
if (typeof formatStr === 'function') { |
|
549
|
|
|
formatStr = formatStr.call(t, options, localeData); |
|
550
|
|
|
} |
|
551
|
|
|
|
|
552
|
|
|
return formatRange(m1, m2, formatStr, null, options.isRTL); |
|
553
|
|
|
}; |
|
554
|
|
|
|
|
555
|
|
|
|
|
556
|
|
|
// Like the vanilla formatDate, but with calendar-specific settings applied. |
|
557
|
|
|
t.formatDate = function(mom, formatStr) { |
|
558
|
|
|
|
|
559
|
|
|
// a function that returns a formatStr // TODO: in future, precompute this |
|
560
|
|
|
if (typeof formatStr === 'function') { |
|
561
|
|
|
formatStr = formatStr.call(t, options, localeData); |
|
562
|
|
|
} |
|
563
|
|
|
|
|
564
|
|
|
return formatDate(mom, formatStr); |
|
565
|
|
|
}; |
|
566
|
|
|
|
|
567
|
|
|
|
|
568
|
|
|
|
|
569
|
|
|
// Imports |
|
570
|
|
|
// ----------------------------------------------------------------------------------- |
|
571
|
|
|
|
|
572
|
|
|
|
|
573
|
|
|
EventManager.call(t, options); |
|
574
|
|
|
var isFetchNeeded = t.isFetchNeeded; |
|
575
|
|
|
var fetchEvents = t.fetchEvents; |
|
576
|
|
|
|
|
577
|
|
|
|
|
578
|
|
|
|
|
579
|
|
|
// Locals |
|
580
|
|
|
// ----------------------------------------------------------------------------------- |
|
581
|
|
|
|
|
582
|
|
|
|
|
583
|
|
|
var _element = element[0]; |
|
584
|
|
|
var header; |
|
585
|
|
|
var headerElement; |
|
586
|
|
|
var content; |
|
587
|
|
|
var tm; // for making theme classes |
|
588
|
|
|
var currentView; |
|
589
|
|
|
var suggestedViewHeight; |
|
590
|
|
|
var windowResizeProxy; // wraps the windowResize function |
|
591
|
|
|
var ignoreWindowResize = 0; |
|
592
|
|
|
var date; |
|
593
|
|
|
var events = []; |
|
594
|
|
|
|
|
595
|
|
|
|
|
596
|
|
|
|
|
597
|
|
|
// Main Rendering |
|
598
|
|
|
// ----------------------------------------------------------------------------------- |
|
599
|
|
|
|
|
600
|
|
|
|
|
601
|
|
|
if (options.defaultDate != null) { |
|
602
|
|
|
date = t.moment(options.defaultDate); |
|
603
|
|
|
} |
|
604
|
|
|
else { |
|
605
|
|
|
date = t.getNow(); |
|
606
|
|
|
} |
|
607
|
|
|
|
|
608
|
|
|
|
|
609
|
|
|
function render(inc) { |
|
610
|
|
|
if (!content) { |
|
611
|
|
|
initialRender(); |
|
612
|
|
|
} |
|
613
|
|
|
else if (elementVisible()) { |
|
614
|
|
|
// mainly for the public API |
|
615
|
|
|
calcSize(); |
|
616
|
|
|
renderView(inc); |
|
617
|
|
|
} |
|
618
|
|
|
} |
|
619
|
|
|
|
|
620
|
|
|
|
|
621
|
|
|
function initialRender() { |
|
622
|
|
|
tm = options.theme ? 'ui' : 'fc'; |
|
|
|
|
|
|
623
|
|
|
element.addClass('fc'); |
|
624
|
|
|
|
|
625
|
|
|
if (options.isRTL) { |
|
626
|
|
|
element.addClass('fc-rtl'); |
|
627
|
|
|
} |
|
628
|
|
|
else { |
|
629
|
|
|
element.addClass('fc-ltr'); |
|
630
|
|
|
} |
|
631
|
|
|
|
|
632
|
|
|
if (options.theme) { |
|
633
|
|
|
element.addClass('ui-widget'); |
|
634
|
|
|
} |
|
635
|
|
|
else { |
|
636
|
|
|
element.addClass('fc-unthemed'); |
|
637
|
|
|
} |
|
638
|
|
|
|
|
639
|
|
|
content = $("<div class='fc-view-container'/>").prependTo(element); |
|
640
|
|
|
|
|
641
|
|
|
header = new Header(t, options); |
|
642
|
|
|
headerElement = header.render(); |
|
643
|
|
|
if (headerElement) { |
|
644
|
|
|
element.prepend(headerElement); |
|
645
|
|
|
} |
|
646
|
|
|
|
|
647
|
|
|
changeView(options.defaultView); |
|
648
|
|
|
|
|
649
|
|
|
if (options.handleWindowResize) { |
|
650
|
|
|
windowResizeProxy = debounce(windowResize, options.windowResizeDelay); // prevents rapid calls |
|
651
|
|
|
$(window).resize(windowResizeProxy); |
|
652
|
|
|
} |
|
653
|
|
|
} |
|
654
|
|
|
|
|
655
|
|
|
|
|
656
|
|
|
function destroy() { |
|
657
|
|
|
|
|
658
|
|
|
if (currentView) { |
|
659
|
|
|
currentView.destroy(); |
|
660
|
|
|
} |
|
661
|
|
|
|
|
662
|
|
|
header.destroy(); |
|
663
|
|
|
content.remove(); |
|
664
|
|
|
element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget'); |
|
665
|
|
|
|
|
666
|
|
|
$(window).unbind('resize', windowResizeProxy); |
|
667
|
|
|
} |
|
668
|
|
|
|
|
669
|
|
|
|
|
670
|
|
|
function elementVisible() { |
|
671
|
|
|
return element.is(':visible'); |
|
672
|
|
|
} |
|
673
|
|
|
|
|
674
|
|
|
|
|
675
|
|
|
|
|
676
|
|
|
// View Rendering |
|
677
|
|
|
// ----------------------------------------------------------------------------------- |
|
678
|
|
|
|
|
679
|
|
|
|
|
680
|
|
|
function changeView(viewName) { |
|
681
|
|
|
renderView(0, viewName); |
|
682
|
|
|
} |
|
683
|
|
|
|
|
684
|
|
|
|
|
685
|
|
|
// Renders a view because of a date change, view-type change, or for the first time |
|
686
|
|
|
function renderView(delta, viewName) { |
|
687
|
|
|
ignoreWindowResize++; |
|
688
|
|
|
|
|
689
|
|
|
// if viewName is changing, destroy the old view |
|
690
|
|
|
if (currentView && viewName && currentView.name !== viewName) { |
|
691
|
|
|
header.deactivateButton(currentView.name); |
|
692
|
|
|
freezeContentHeight(); // prevent a scroll jump when view element is removed |
|
693
|
|
|
if (currentView.start) { // rendered before? |
|
694
|
|
|
currentView.destroy(); |
|
695
|
|
|
} |
|
696
|
|
|
currentView.el.remove(); |
|
697
|
|
|
currentView = null; |
|
698
|
|
|
} |
|
699
|
|
|
|
|
700
|
|
|
// if viewName changed, or the view was never created, create a fresh view |
|
701
|
|
|
if (!currentView && viewName) { |
|
702
|
|
|
currentView = new fcViews[viewName](t); |
|
703
|
|
|
currentView.el = $("<div class='fc-view fc-" + viewName + "-view' />").appendTo(content); |
|
704
|
|
|
header.activateButton(viewName); |
|
705
|
|
|
} |
|
706
|
|
|
|
|
707
|
|
|
if (currentView) { |
|
708
|
|
|
|
|
709
|
|
|
// let the view determine what the delta means |
|
710
|
|
|
if (delta) { |
|
711
|
|
|
date = currentView.incrementDate(date, delta); |
|
712
|
|
|
} |
|
713
|
|
|
|
|
714
|
|
|
// render or rerender the view |
|
715
|
|
|
if ( |
|
716
|
|
|
!currentView.start || // never rendered before |
|
717
|
|
|
delta || // explicit date window change |
|
718
|
|
|
!date.isWithin(currentView.intervalStart, currentView.intervalEnd) // implicit date window change |
|
719
|
|
|
) { |
|
720
|
|
|
if (elementVisible()) { |
|
721
|
|
|
|
|
722
|
|
|
freezeContentHeight(); |
|
723
|
|
|
if (currentView.start) { // rendered before? |
|
724
|
|
|
currentView.destroy(); |
|
725
|
|
|
} |
|
726
|
|
|
currentView.render(date); |
|
727
|
|
|
unfreezeContentHeight(); |
|
728
|
|
|
|
|
729
|
|
|
// need to do this after View::render, so dates are calculated |
|
730
|
|
|
updateTitle(); |
|
731
|
|
|
updateTodayButton(); |
|
732
|
|
|
|
|
733
|
|
|
getAndRenderEvents(); |
|
734
|
|
|
} |
|
735
|
|
|
} |
|
736
|
|
|
} |
|
737
|
|
|
|
|
738
|
|
|
unfreezeContentHeight(); // undo any lone freezeContentHeight calls |
|
739
|
|
|
ignoreWindowResize--; |
|
740
|
|
|
} |
|
741
|
|
|
|
|
742
|
|
|
|
|
743
|
|
|
|
|
744
|
|
|
// Resizing |
|
745
|
|
|
// ----------------------------------------------------------------------------------- |
|
746
|
|
|
|
|
747
|
|
|
|
|
748
|
|
|
t.getSuggestedViewHeight = function() { |
|
749
|
|
|
if (suggestedViewHeight === undefined) { |
|
750
|
|
|
calcSize(); |
|
751
|
|
|
} |
|
752
|
|
|
return suggestedViewHeight; |
|
753
|
|
|
}; |
|
754
|
|
|
|
|
755
|
|
|
|
|
756
|
|
|
t.isHeightAuto = function() { |
|
757
|
|
|
return options.contentHeight === 'auto' || options.height === 'auto'; |
|
758
|
|
|
}; |
|
759
|
|
|
|
|
760
|
|
|
|
|
761
|
|
|
function updateSize(shouldRecalc) { |
|
762
|
|
|
if (elementVisible()) { |
|
763
|
|
|
|
|
764
|
|
|
if (shouldRecalc) { |
|
765
|
|
|
_calcSize(); |
|
766
|
|
|
} |
|
767
|
|
|
|
|
768
|
|
|
ignoreWindowResize++; |
|
769
|
|
|
currentView.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto() |
|
770
|
|
|
ignoreWindowResize--; |
|
771
|
|
|
|
|
772
|
|
|
return true; // signal success |
|
773
|
|
|
} |
|
774
|
|
|
} |
|
775
|
|
|
|
|
776
|
|
|
|
|
777
|
|
|
function calcSize() { |
|
778
|
|
|
if (elementVisible()) { |
|
779
|
|
|
_calcSize(); |
|
780
|
|
|
} |
|
781
|
|
|
} |
|
782
|
|
|
|
|
783
|
|
|
|
|
784
|
|
|
function _calcSize() { // assumes elementVisible |
|
785
|
|
|
if (typeof options.contentHeight === 'number') { // exists and not 'auto' |
|
786
|
|
|
suggestedViewHeight = options.contentHeight; |
|
787
|
|
|
} |
|
788
|
|
|
else if (typeof options.height === 'number') { // exists and not 'auto' |
|
789
|
|
|
suggestedViewHeight = options.height - (headerElement ? headerElement.outerHeight(true) : 0); |
|
790
|
|
|
} |
|
791
|
|
|
else { |
|
792
|
|
|
suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5)); |
|
793
|
|
|
} |
|
794
|
|
|
} |
|
795
|
|
|
|
|
796
|
|
|
|
|
797
|
|
|
function windowResize(ev) { |
|
798
|
|
|
if ( |
|
799
|
|
|
!ignoreWindowResize && |
|
800
|
|
|
ev.target === window && // so we don't process jqui "resize" events that have bubbled up |
|
801
|
|
|
currentView.start // view has already been rendered |
|
802
|
|
|
) { |
|
803
|
|
|
if (updateSize(true)) { |
|
804
|
|
|
currentView.trigger('windowResize', _element); |
|
805
|
|
|
} |
|
806
|
|
|
} |
|
807
|
|
|
} |
|
808
|
|
|
|
|
809
|
|
|
|
|
810
|
|
|
|
|
811
|
|
|
/* Event Fetching/Rendering |
|
812
|
|
|
-----------------------------------------------------------------------------*/ |
|
813
|
|
|
// TODO: going forward, most of this stuff should be directly handled by the view |
|
814
|
|
|
|
|
815
|
|
|
|
|
816
|
|
|
function refetchEvents() { // can be called as an API method |
|
817
|
|
|
destroyEvents(); // so that events are cleared before user starts waiting for AJAX |
|
818
|
|
|
fetchAndRenderEvents(); |
|
819
|
|
|
} |
|
820
|
|
|
|
|
821
|
|
|
|
|
822
|
|
|
function renderEvents() { // destroys old events if previously rendered |
|
823
|
|
|
if (elementVisible()) { |
|
824
|
|
|
freezeContentHeight(); |
|
825
|
|
|
currentView.destroyEvents(); // no performance cost if never rendered |
|
826
|
|
|
currentView.renderEvents(events); |
|
827
|
|
|
unfreezeContentHeight(); |
|
828
|
|
|
} |
|
829
|
|
|
} |
|
830
|
|
|
|
|
831
|
|
|
|
|
832
|
|
|
function destroyEvents() { |
|
833
|
|
|
freezeContentHeight(); |
|
834
|
|
|
currentView.destroyEvents(); |
|
835
|
|
|
unfreezeContentHeight(); |
|
836
|
|
|
} |
|
837
|
|
|
|
|
838
|
|
|
|
|
839
|
|
|
function getAndRenderEvents() { |
|
840
|
|
|
if (!options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) { |
|
841
|
|
|
fetchAndRenderEvents(); |
|
842
|
|
|
} |
|
843
|
|
|
else { |
|
844
|
|
|
renderEvents(); |
|
845
|
|
|
} |
|
846
|
|
|
} |
|
847
|
|
|
|
|
848
|
|
|
|
|
849
|
|
|
function fetchAndRenderEvents() { |
|
850
|
|
|
fetchEvents(currentView.start, currentView.end); |
|
851
|
|
|
// ... will call reportEvents |
|
852
|
|
|
// ... which will call renderEvents |
|
853
|
|
|
} |
|
854
|
|
|
|
|
855
|
|
|
|
|
856
|
|
|
// called when event data arrives |
|
857
|
|
|
function reportEvents(_events) { |
|
858
|
|
|
events = _events; |
|
859
|
|
|
renderEvents(); |
|
860
|
|
|
} |
|
861
|
|
|
|
|
862
|
|
|
|
|
863
|
|
|
// called when a single event's data has been changed |
|
864
|
|
|
function reportEventChange() { |
|
865
|
|
|
renderEvents(); |
|
866
|
|
|
} |
|
867
|
|
|
|
|
868
|
|
|
|
|
869
|
|
|
|
|
870
|
|
|
/* Header Updating |
|
871
|
|
|
-----------------------------------------------------------------------------*/ |
|
872
|
|
|
|
|
873
|
|
|
|
|
874
|
|
|
function updateTitle() { |
|
875
|
|
|
header.updateTitle(currentView.title); |
|
876
|
|
|
} |
|
877
|
|
|
|
|
878
|
|
|
|
|
879
|
|
|
function updateTodayButton() { |
|
880
|
|
|
var now = t.getNow(); |
|
881
|
|
|
if (now.isWithin(currentView.intervalStart, currentView.intervalEnd)) { |
|
882
|
|
|
header.disableButton('today'); |
|
883
|
|
|
} |
|
884
|
|
|
else { |
|
885
|
|
|
header.enableButton('today'); |
|
886
|
|
|
} |
|
887
|
|
|
} |
|
888
|
|
|
|
|
889
|
|
|
|
|
890
|
|
|
|
|
891
|
|
|
/* Selection |
|
892
|
|
|
-----------------------------------------------------------------------------*/ |
|
893
|
|
|
|
|
894
|
|
|
|
|
895
|
|
|
function select(start, end) { |
|
896
|
|
|
|
|
897
|
|
|
start = t.moment(start); |
|
898
|
|
|
if (end) { |
|
899
|
|
|
end = t.moment(end); |
|
900
|
|
|
} |
|
901
|
|
|
else if (start.hasTime()) { |
|
902
|
|
|
end = start.clone().add(t.defaultTimedEventDuration); |
|
903
|
|
|
} |
|
904
|
|
|
else { |
|
905
|
|
|
end = start.clone().add(t.defaultAllDayEventDuration); |
|
906
|
|
|
} |
|
907
|
|
|
|
|
908
|
|
|
currentView.select(start, end); |
|
909
|
|
|
} |
|
910
|
|
|
|
|
911
|
|
|
|
|
912
|
|
|
function unselect() { // safe to be called before renderView |
|
913
|
|
|
if (currentView) { |
|
914
|
|
|
currentView.unselect(); |
|
915
|
|
|
} |
|
916
|
|
|
} |
|
917
|
|
|
|
|
918
|
|
|
|
|
919
|
|
|
|
|
920
|
|
|
/* Date |
|
921
|
|
|
-----------------------------------------------------------------------------*/ |
|
922
|
|
|
|
|
923
|
|
|
|
|
924
|
|
|
function prev() { |
|
925
|
|
|
renderView(-1); |
|
926
|
|
|
} |
|
927
|
|
|
|
|
928
|
|
|
|
|
929
|
|
|
function next() { |
|
930
|
|
|
renderView(1); |
|
931
|
|
|
} |
|
932
|
|
|
|
|
933
|
|
|
|
|
934
|
|
|
function prevYear() { |
|
935
|
|
|
date.add(-1, 'years'); |
|
936
|
|
|
renderView(); |
|
937
|
|
|
} |
|
938
|
|
|
|
|
939
|
|
|
|
|
940
|
|
|
function nextYear() { |
|
941
|
|
|
date.add(1, 'years'); |
|
942
|
|
|
renderView(); |
|
943
|
|
|
} |
|
944
|
|
|
|
|
945
|
|
|
|
|
946
|
|
|
function today() { |
|
947
|
|
|
date = t.getNow(); |
|
948
|
|
|
renderView(); |
|
949
|
|
|
} |
|
950
|
|
|
|
|
951
|
|
|
|
|
952
|
|
|
function gotoDate(dateInput) { |
|
953
|
|
|
date = t.moment(dateInput); |
|
954
|
|
|
renderView(); |
|
955
|
|
|
} |
|
956
|
|
|
|
|
957
|
|
|
|
|
958
|
|
|
function incrementDate(delta) { |
|
959
|
|
|
date.add(moment.duration(delta)); |
|
960
|
|
|
renderView(); |
|
961
|
|
|
} |
|
962
|
|
|
|
|
963
|
|
|
|
|
964
|
|
|
// Forces navigation to a view for the given date. |
|
965
|
|
|
// `viewName` can be a specific view name or a generic one like "week" or "day". |
|
966
|
|
|
function zoomTo(newDate, viewName) { |
|
967
|
|
|
var viewStr; |
|
968
|
|
|
var match; |
|
969
|
|
|
|
|
970
|
|
|
if (!viewName || fcViews[viewName] === undefined) { // a general view name, or "auto" |
|
971
|
|
|
viewName = viewName || 'day'; |
|
972
|
|
|
viewStr = header.getViewsWithButtons().join(' '); // space-separated string of all the views in the header |
|
973
|
|
|
|
|
974
|
|
|
// try to match a general view name, like "week", against a specific one, like "agendaWeek" |
|
975
|
|
|
match = viewStr.match(new RegExp('\\w+' + capitaliseFirstLetter(viewName))); |
|
976
|
|
|
|
|
977
|
|
|
// fall back to the day view being used in the header |
|
978
|
|
|
if (!match) { |
|
979
|
|
|
match = viewStr.match(/\w+Day/); |
|
980
|
|
|
} |
|
981
|
|
|
|
|
982
|
|
|
viewName = match ? match[0] : 'agendaDay'; // fall back to agendaDay |
|
983
|
|
|
} |
|
984
|
|
|
|
|
985
|
|
|
date = newDate; |
|
986
|
|
|
changeView(viewName); |
|
987
|
|
|
} |
|
988
|
|
|
|
|
989
|
|
|
|
|
990
|
|
|
function getDate() { |
|
991
|
|
|
return date.clone(); |
|
992
|
|
|
} |
|
993
|
|
|
|
|
994
|
|
|
|
|
995
|
|
|
|
|
996
|
|
|
/* Height "Freezing" |
|
997
|
|
|
-----------------------------------------------------------------------------*/ |
|
998
|
|
|
|
|
999
|
|
|
|
|
1000
|
|
|
function freezeContentHeight() { |
|
1001
|
|
|
content.css({ |
|
1002
|
|
|
width: '100%', |
|
1003
|
|
|
height: content.height(), |
|
1004
|
|
|
overflow: 'hidden' |
|
1005
|
|
|
}); |
|
1006
|
|
|
} |
|
1007
|
|
|
|
|
1008
|
|
|
|
|
1009
|
|
|
function unfreezeContentHeight() { |
|
1010
|
|
|
content.css({ |
|
1011
|
|
|
width: '', |
|
1012
|
|
|
height: '', |
|
1013
|
|
|
overflow: '' |
|
1014
|
|
|
}); |
|
1015
|
|
|
} |
|
1016
|
|
|
|
|
1017
|
|
|
|
|
1018
|
|
|
|
|
1019
|
|
|
/* Misc |
|
1020
|
|
|
-----------------------------------------------------------------------------*/ |
|
1021
|
|
|
|
|
1022
|
|
|
|
|
1023
|
|
|
function getCalendar() { |
|
1024
|
|
|
return t; |
|
1025
|
|
|
} |
|
1026
|
|
|
|
|
1027
|
|
|
|
|
1028
|
|
|
function getView() { |
|
1029
|
|
|
return currentView; |
|
1030
|
|
|
} |
|
1031
|
|
|
|
|
1032
|
|
|
|
|
1033
|
|
|
function option(name, value) { |
|
1034
|
|
|
if (value === undefined) { |
|
1035
|
|
|
return options[name]; |
|
1036
|
|
|
} |
|
1037
|
|
|
if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') { |
|
1038
|
|
|
options[name] = value; |
|
1039
|
|
|
updateSize(true); // true = allow recalculation of height |
|
|
|
|
|
|
1040
|
|
|
} |
|
1041
|
|
|
} |
|
1042
|
|
|
|
|
1043
|
|
|
|
|
1044
|
|
|
function trigger(name, thisObj) { |
|
1045
|
|
|
if (options[name]) { |
|
1046
|
|
|
return options[name].apply( |
|
1047
|
|
|
thisObj || _element, |
|
1048
|
|
|
Array.prototype.slice.call(arguments, 2) |
|
1049
|
|
|
); |
|
1050
|
|
|
} |
|
1051
|
|
|
} |
|
1052
|
|
|
|
|
1053
|
|
|
} |
|
1054
|
|
|
|
|
1055
|
|
|
;; |
|
1056
|
|
|
|
|
1057
|
|
|
/* Top toolbar area with buttons and title |
|
1058
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
|
1059
|
|
|
// TODO: rename all header-related things to "toolbar" |
|
1060
|
|
|
|
|
1061
|
|
|
function Header(calendar, options) { |
|
1062
|
|
|
var t = this; |
|
1063
|
|
|
|
|
1064
|
|
|
// exports |
|
1065
|
|
|
t.render = render; |
|
1066
|
|
|
t.destroy = destroy; |
|
1067
|
|
|
t.updateTitle = updateTitle; |
|
1068
|
|
|
t.activateButton = activateButton; |
|
1069
|
|
|
t.deactivateButton = deactivateButton; |
|
1070
|
|
|
t.disableButton = disableButton; |
|
1071
|
|
|
t.enableButton = enableButton; |
|
1072
|
|
|
t.getViewsWithButtons = getViewsWithButtons; |
|
1073
|
|
|
|
|
1074
|
|
|
// locals |
|
1075
|
|
|
var el = $(); |
|
1076
|
|
|
var viewsWithButtons = []; |
|
1077
|
|
|
var tm; |
|
1078
|
|
|
|
|
1079
|
|
|
|
|
1080
|
|
|
function render() { |
|
1081
|
|
|
var sections = options.header; |
|
1082
|
|
|
|
|
1083
|
|
|
tm = options.theme ? 'ui' : 'fc'; |
|
1084
|
|
|
|
|
1085
|
|
|
if (sections) { |
|
1086
|
|
|
el = $("<div class='fc-toolbar'/>") |
|
1087
|
|
|
.append(renderSection('left')) |
|
1088
|
|
|
.append(renderSection('right')) |
|
1089
|
|
|
.append(renderSection('center')) |
|
1090
|
|
|
.append('<div class="fc-clear"/>'); |
|
1091
|
|
|
|
|
1092
|
|
|
return el; |
|
1093
|
|
|
} |
|
1094
|
|
|
} |
|
1095
|
|
|
|
|
1096
|
|
|
|
|
1097
|
|
|
function destroy() { |
|
1098
|
|
|
el.remove(); |
|
1099
|
|
|
} |
|
1100
|
|
|
|
|
1101
|
|
|
|
|
1102
|
|
|
function renderSection(position) { |
|
1103
|
|
|
var sectionEl = $('<div class="fc-' + position + '"/>'); |
|
1104
|
|
|
var buttonStr = options.header[position]; |
|
1105
|
|
|
|
|
1106
|
|
|
if (buttonStr) { |
|
1107
|
|
|
$.each(buttonStr.split(' '), function(i) { |
|
1108
|
|
|
var groupChildren = $(); |
|
1109
|
|
|
var isOnlyButtons = true; |
|
1110
|
|
|
var groupEl; |
|
1111
|
|
|
|
|
1112
|
|
|
$.each(this.split(','), function(j, buttonName) { |
|
1113
|
|
|
var buttonClick; |
|
1114
|
|
|
var themeIcon; |
|
1115
|
|
|
var normalIcon; |
|
1116
|
|
|
var defaultText; |
|
1117
|
|
|
var customText; |
|
1118
|
|
|
var innerHtml; |
|
1119
|
|
|
var classes; |
|
1120
|
|
|
var button; |
|
1121
|
|
|
|
|
1122
|
|
|
if (buttonName == 'title') { |
|
1123
|
|
|
groupChildren = groupChildren.add($('<h2> </h2>')); // we always want it to take up height |
|
1124
|
|
|
isOnlyButtons = false; |
|
1125
|
|
|
} |
|
1126
|
|
|
else { |
|
1127
|
|
|
if (calendar[buttonName]) { // a calendar method |
|
1128
|
|
|
buttonClick = function() { |
|
1129
|
|
|
calendar[buttonName](); |
|
1130
|
|
|
}; |
|
1131
|
|
|
} |
|
1132
|
|
|
else if (fcViews[buttonName]) { // a view name |
|
1133
|
|
|
buttonClick = function() { |
|
1134
|
|
|
calendar.changeView(buttonName); |
|
1135
|
|
|
}; |
|
1136
|
|
|
viewsWithButtons.push(buttonName); |
|
1137
|
|
|
} |
|
1138
|
|
|
if (buttonClick) { |
|
1139
|
|
|
|
|
1140
|
|
|
// smartProperty allows different text per view button (ex: "Agenda Week" vs "Basic Week") |
|
1141
|
|
|
themeIcon = smartProperty(options.themeButtonIcons, buttonName); |
|
1142
|
|
|
normalIcon = smartProperty(options.buttonIcons, buttonName); |
|
1143
|
|
|
defaultText = smartProperty(options.defaultButtonText, buttonName); |
|
1144
|
|
|
customText = smartProperty(options.buttonText, buttonName); |
|
1145
|
|
|
|
|
1146
|
|
|
if (customText) { |
|
1147
|
|
|
innerHtml = htmlEscape(customText); |
|
1148
|
|
|
} |
|
1149
|
|
|
else if (themeIcon && options.theme) { |
|
1150
|
|
|
innerHtml = "<span class='ui-icon ui-icon-" + themeIcon + "'></span>"; |
|
1151
|
|
|
} |
|
1152
|
|
|
else if (normalIcon && !options.theme) { |
|
1153
|
|
|
innerHtml = "<span class='fc-icon fc-icon-" + normalIcon + "'></span>"; |
|
1154
|
|
|
} |
|
1155
|
|
|
else { |
|
1156
|
|
|
innerHtml = htmlEscape(defaultText || buttonName); |
|
1157
|
|
|
} |
|
1158
|
|
|
|
|
1159
|
|
|
classes = [ |
|
1160
|
|
|
'fc-' + buttonName + '-button', |
|
1161
|
|
|
tm + '-button', |
|
1162
|
|
|
tm + '-state-default' |
|
1163
|
|
|
]; |
|
1164
|
|
|
|
|
1165
|
|
|
button = $( // type="button" so that it doesn't submit a form |
|
1166
|
|
|
'<button type="button" class="' + classes.join(' ') + '">' + |
|
1167
|
|
|
innerHtml + |
|
1168
|
|
|
'</button>' |
|
1169
|
|
|
) |
|
1170
|
|
|
.click(function() { |
|
1171
|
|
|
// don't process clicks for disabled buttons |
|
1172
|
|
|
if (!button.hasClass(tm + '-state-disabled')) { |
|
1173
|
|
|
|
|
1174
|
|
|
buttonClick(); |
|
1175
|
|
|
|
|
1176
|
|
|
// after the click action, if the button becomes the "active" tab, or disabled, |
|
1177
|
|
|
// it should never have a hover class, so remove it now. |
|
1178
|
|
|
if ( |
|
1179
|
|
|
button.hasClass(tm + '-state-active') || |
|
1180
|
|
|
button.hasClass(tm + '-state-disabled') |
|
1181
|
|
|
) { |
|
1182
|
|
|
button.removeClass(tm + '-state-hover'); |
|
1183
|
|
|
} |
|
1184
|
|
|
} |
|
1185
|
|
|
}) |
|
1186
|
|
|
.mousedown(function() { |
|
1187
|
|
|
// the *down* effect (mouse pressed in). |
|
1188
|
|
|
// only on buttons that are not the "active" tab, or disabled |
|
1189
|
|
|
button |
|
1190
|
|
|
.not('.' + tm + '-state-active') |
|
1191
|
|
|
.not('.' + tm + '-state-disabled') |
|
1192
|
|
|
.addClass(tm + '-state-down'); |
|
1193
|
|
|
}) |
|
1194
|
|
|
.mouseup(function() { |
|
1195
|
|
|
// undo the *down* effect |
|
1196
|
|
|
button.removeClass(tm + '-state-down'); |
|
1197
|
|
|
}) |
|
1198
|
|
|
.hover( |
|
1199
|
|
|
function() { |
|
1200
|
|
|
// the *hover* effect. |
|
1201
|
|
|
// only on buttons that are not the "active" tab, or disabled |
|
1202
|
|
|
button |
|
1203
|
|
|
.not('.' + tm + '-state-active') |
|
1204
|
|
|
.not('.' + tm + '-state-disabled') |
|
1205
|
|
|
.addClass(tm + '-state-hover'); |
|
1206
|
|
|
}, |
|
1207
|
|
|
function() { |
|
1208
|
|
|
// undo the *hover* effect |
|
1209
|
|
|
button |
|
1210
|
|
|
.removeClass(tm + '-state-hover') |
|
1211
|
|
|
.removeClass(tm + '-state-down'); // if mouseleave happens before mouseup |
|
1212
|
|
|
} |
|
1213
|
|
|
); |
|
1214
|
|
|
|
|
1215
|
|
|
groupChildren = groupChildren.add(button); |
|
1216
|
|
|
} |
|
1217
|
|
|
} |
|
1218
|
|
|
}); |
|
1219
|
|
|
|
|
1220
|
|
|
if (isOnlyButtons) { |
|
1221
|
|
|
groupChildren |
|
1222
|
|
|
.first().addClass(tm + '-corner-left').end() |
|
1223
|
|
|
.last().addClass(tm + '-corner-right').end(); |
|
1224
|
|
|
} |
|
1225
|
|
|
|
|
1226
|
|
|
if (groupChildren.length > 1) { |
|
1227
|
|
|
groupEl = $('<div/>'); |
|
1228
|
|
|
if (isOnlyButtons) { |
|
1229
|
|
|
groupEl.addClass('fc-button-group'); |
|
1230
|
|
|
} |
|
1231
|
|
|
groupEl.append(groupChildren); |
|
1232
|
|
|
sectionEl.append(groupEl); |
|
1233
|
|
|
} |
|
1234
|
|
|
else { |
|
1235
|
|
|
sectionEl.append(groupChildren); // 1 or 0 children |
|
1236
|
|
|
} |
|
1237
|
|
|
}); |
|
1238
|
|
|
} |
|
1239
|
|
|
|
|
1240
|
|
|
return sectionEl; |
|
1241
|
|
|
} |
|
1242
|
|
|
|
|
1243
|
|
|
|
|
1244
|
|
|
function updateTitle(text) { |
|
1245
|
|
|
el.find('h2').text(text); |
|
1246
|
|
|
} |
|
1247
|
|
|
|
|
1248
|
|
|
|
|
1249
|
|
|
function activateButton(buttonName) { |
|
1250
|
|
|
el.find('.fc-' + buttonName + '-button') |
|
1251
|
|
|
.addClass(tm + '-state-active'); |
|
1252
|
|
|
} |
|
1253
|
|
|
|
|
1254
|
|
|
|
|
1255
|
|
|
function deactivateButton(buttonName) { |
|
1256
|
|
|
el.find('.fc-' + buttonName + '-button') |
|
1257
|
|
|
.removeClass(tm + '-state-active'); |
|
1258
|
|
|
} |
|
1259
|
|
|
|
|
1260
|
|
|
|
|
1261
|
|
|
function disableButton(buttonName) { |
|
1262
|
|
|
el.find('.fc-' + buttonName + '-button') |
|
1263
|
|
|
.attr('disabled', 'disabled') |
|
1264
|
|
|
.addClass(tm + '-state-disabled'); |
|
1265
|
|
|
} |
|
1266
|
|
|
|
|
1267
|
|
|
|
|
1268
|
|
|
function enableButton(buttonName) { |
|
1269
|
|
|
el.find('.fc-' + buttonName + '-button') |
|
1270
|
|
|
.removeAttr('disabled') |
|
1271
|
|
|
.removeClass(tm + '-state-disabled'); |
|
1272
|
|
|
} |
|
1273
|
|
|
|
|
1274
|
|
|
|
|
1275
|
|
|
function getViewsWithButtons() { |
|
1276
|
|
|
return viewsWithButtons; |
|
1277
|
|
|
} |
|
1278
|
|
|
|
|
1279
|
|
|
} |
|
1280
|
|
|
|
|
1281
|
|
|
;; |
|
1282
|
|
|
|
|
1283
|
|
|
fc.sourceNormalizers = []; |
|
1284
|
|
|
fc.sourceFetchers = []; |
|
1285
|
|
|
|
|
1286
|
|
|
var ajaxDefaults = { |
|
1287
|
|
|
dataType: 'json', |
|
1288
|
|
|
cache: false |
|
1289
|
|
|
}; |
|
1290
|
|
|
|
|
1291
|
|
|
var eventGUID = 1; |
|
1292
|
|
|
|
|
1293
|
|
|
|
|
1294
|
|
|
function EventManager(options) { // assumed to be a calendar |
|
1295
|
|
|
var t = this; |
|
1296
|
|
|
|
|
1297
|
|
|
|
|
1298
|
|
|
// exports |
|
1299
|
|
|
t.isFetchNeeded = isFetchNeeded; |
|
1300
|
|
|
t.fetchEvents = fetchEvents; |
|
1301
|
|
|
t.addEventSource = addEventSource; |
|
1302
|
|
|
t.removeEventSource = removeEventSource; |
|
1303
|
|
|
t.updateEvent = updateEvent; |
|
1304
|
|
|
t.renderEvent = renderEvent; |
|
1305
|
|
|
t.removeEvents = removeEvents; |
|
1306
|
|
|
t.clientEvents = clientEvents; |
|
1307
|
|
|
t.mutateEvent = mutateEvent; |
|
1308
|
|
|
|
|
1309
|
|
|
|
|
1310
|
|
|
// imports |
|
1311
|
|
|
var trigger = t.trigger; |
|
1312
|
|
|
var getView = t.getView; |
|
1313
|
|
|
var reportEvents = t.reportEvents; |
|
1314
|
|
|
var getEventEnd = t.getEventEnd; |
|
1315
|
|
|
|
|
1316
|
|
|
|
|
1317
|
|
|
// locals |
|
1318
|
|
|
var stickySource = { events: [] }; |
|
1319
|
|
|
var sources = [ stickySource ]; |
|
1320
|
|
|
var rangeStart, rangeEnd; |
|
1321
|
|
|
var currentFetchID = 0; |
|
1322
|
|
|
var pendingSourceCnt = 0; |
|
1323
|
|
|
var loadingLevel = 0; |
|
1324
|
|
|
var cache = []; |
|
1325
|
|
|
|
|
1326
|
|
|
|
|
1327
|
|
|
$.each( |
|
1328
|
|
|
(options.events ? [ options.events ] : []).concat(options.eventSources || []), |
|
1329
|
|
|
function(i, sourceInput) { |
|
1330
|
|
|
var source = buildEventSource(sourceInput); |
|
1331
|
|
|
if (source) { |
|
1332
|
|
|
sources.push(source); |
|
1333
|
|
|
} |
|
1334
|
|
|
} |
|
1335
|
|
|
); |
|
1336
|
|
|
|
|
1337
|
|
|
|
|
1338
|
|
|
|
|
1339
|
|
|
/* Fetching |
|
1340
|
|
|
-----------------------------------------------------------------------------*/ |
|
1341
|
|
|
|
|
1342
|
|
|
|
|
1343
|
|
|
function isFetchNeeded(start, end) { |
|
1344
|
|
|
return !rangeStart || // nothing has been fetched yet? |
|
1345
|
|
|
// or, a part of the new range is outside of the old range? (after normalizing) |
|
1346
|
|
|
start.clone().stripZone() < rangeStart.clone().stripZone() || |
|
1347
|
|
|
end.clone().stripZone() > rangeEnd.clone().stripZone(); |
|
1348
|
|
|
} |
|
1349
|
|
|
|
|
1350
|
|
|
|
|
1351
|
|
|
function fetchEvents(start, end) { |
|
1352
|
|
|
rangeStart = start; |
|
1353
|
|
|
rangeEnd = end; |
|
1354
|
|
|
cache = []; |
|
1355
|
|
|
var fetchID = ++currentFetchID; |
|
1356
|
|
|
var len = sources.length; |
|
1357
|
|
|
pendingSourceCnt = len; |
|
1358
|
|
|
for (var i=0; i<len; i++) { |
|
1359
|
|
|
fetchEventSource(sources[i], fetchID); |
|
1360
|
|
|
} |
|
1361
|
|
|
} |
|
1362
|
|
|
|
|
1363
|
|
|
|
|
1364
|
|
|
function fetchEventSource(source, fetchID) { |
|
1365
|
|
|
_fetchEventSource(source, function(events) { |
|
1366
|
|
|
var isArraySource = $.isArray(source.events); |
|
1367
|
|
|
var i; |
|
1368
|
|
|
var event; |
|
1369
|
|
|
|
|
1370
|
|
|
if (fetchID == currentFetchID) { |
|
1371
|
|
|
|
|
1372
|
|
|
if (events) { |
|
1373
|
|
|
for (i=0; i<events.length; i++) { |
|
1374
|
|
|
event = events[i]; |
|
1375
|
|
|
|
|
1376
|
|
|
// event array sources have already been convert to Event Objects |
|
1377
|
|
|
if (!isArraySource) { |
|
1378
|
|
|
event = buildEvent(event, source); |
|
1379
|
|
|
} |
|
1380
|
|
|
|
|
1381
|
|
|
if (event) { |
|
1382
|
|
|
cache.push(event); |
|
1383
|
|
|
} |
|
1384
|
|
|
} |
|
1385
|
|
|
} |
|
1386
|
|
|
|
|
1387
|
|
|
pendingSourceCnt--; |
|
1388
|
|
|
if (!pendingSourceCnt) { |
|
1389
|
|
|
reportEvents(cache); |
|
1390
|
|
|
} |
|
1391
|
|
|
} |
|
1392
|
|
|
}); |
|
1393
|
|
|
} |
|
1394
|
|
|
|
|
1395
|
|
|
|
|
1396
|
|
|
function _fetchEventSource(source, callback) { |
|
1397
|
|
|
var i; |
|
1398
|
|
|
var fetchers = fc.sourceFetchers; |
|
1399
|
|
|
var res; |
|
1400
|
|
|
|
|
1401
|
|
|
for (i=0; i<fetchers.length; i++) { |
|
1402
|
|
|
res = fetchers[i].call( |
|
1403
|
|
|
t, // this, the Calendar object |
|
1404
|
|
|
source, |
|
1405
|
|
|
rangeStart.clone(), |
|
1406
|
|
|
rangeEnd.clone(), |
|
1407
|
|
|
options.timezone, |
|
1408
|
|
|
callback |
|
1409
|
|
|
); |
|
1410
|
|
|
|
|
1411
|
|
|
if (res === true) { |
|
1412
|
|
|
// the fetcher is in charge. made its own async request |
|
1413
|
|
|
return; |
|
1414
|
|
|
} |
|
1415
|
|
|
else if (typeof res == 'object') { |
|
1416
|
|
|
// the fetcher returned a new source. process it |
|
1417
|
|
|
_fetchEventSource(res, callback); |
|
1418
|
|
|
return; |
|
1419
|
|
|
} |
|
1420
|
|
|
} |
|
1421
|
|
|
|
|
1422
|
|
|
var events = source.events; |
|
1423
|
|
|
if (events) { |
|
1424
|
|
|
if ($.isFunction(events)) { |
|
1425
|
|
|
pushLoading(); |
|
1426
|
|
|
events.call( |
|
1427
|
|
|
t, // this, the Calendar object |
|
1428
|
|
|
rangeStart.clone(), |
|
1429
|
|
|
rangeEnd.clone(), |
|
1430
|
|
|
options.timezone, |
|
1431
|
|
|
function(events) { |
|
1432
|
|
|
callback(events); |
|
1433
|
|
|
popLoading(); |
|
1434
|
|
|
} |
|
1435
|
|
|
); |
|
1436
|
|
|
} |
|
1437
|
|
|
else if ($.isArray(events)) { |
|
1438
|
|
|
callback(events); |
|
1439
|
|
|
} |
|
1440
|
|
|
else { |
|
1441
|
|
|
callback(); |
|
1442
|
|
|
} |
|
1443
|
|
|
}else{ |
|
1444
|
|
|
var url = source.url; |
|
1445
|
|
|
if (url) { |
|
1446
|
|
|
var success = source.success; |
|
1447
|
|
|
var error = source.error; |
|
1448
|
|
|
var complete = source.complete; |
|
1449
|
|
|
|
|
1450
|
|
|
// retrieve any outbound GET/POST $.ajax data from the options |
|
1451
|
|
|
var customData; |
|
1452
|
|
|
if ($.isFunction(source.data)) { |
|
1453
|
|
|
// supplied as a function that returns a key/value object |
|
1454
|
|
|
customData = source.data(); |
|
1455
|
|
|
} |
|
1456
|
|
|
else { |
|
1457
|
|
|
// supplied as a straight key/value object |
|
1458
|
|
|
customData = source.data; |
|
1459
|
|
|
} |
|
1460
|
|
|
|
|
1461
|
|
|
// use a copy of the custom data so we can modify the parameters |
|
1462
|
|
|
// and not affect the passed-in object. |
|
1463
|
|
|
var data = $.extend({}, customData || {}); |
|
1464
|
|
|
|
|
1465
|
|
|
var startParam = firstDefined(source.startParam, options.startParam); |
|
1466
|
|
|
var endParam = firstDefined(source.endParam, options.endParam); |
|
1467
|
|
|
var timezoneParam = firstDefined(source.timezoneParam, options.timezoneParam); |
|
1468
|
|
|
|
|
1469
|
|
|
if (startParam) { |
|
1470
|
|
|
data[startParam] = rangeStart.format(); |
|
1471
|
|
|
} |
|
1472
|
|
|
if (endParam) { |
|
1473
|
|
|
data[endParam] = rangeEnd.format(); |
|
1474
|
|
|
} |
|
1475
|
|
|
if (options.timezone && options.timezone != 'local') { |
|
1476
|
|
|
data[timezoneParam] = options.timezone; |
|
1477
|
|
|
} |
|
1478
|
|
|
|
|
1479
|
|
|
pushLoading(); |
|
1480
|
|
|
$.ajax($.extend({}, ajaxDefaults, source, { |
|
1481
|
|
|
data: data, |
|
1482
|
|
|
success: function(events) { |
|
1483
|
|
|
events = events || []; |
|
1484
|
|
|
var res = applyAll(success, this, arguments); |
|
1485
|
|
|
if ($.isArray(res)) { |
|
1486
|
|
|
events = res; |
|
1487
|
|
|
} |
|
1488
|
|
|
callback(events); |
|
1489
|
|
|
}, |
|
1490
|
|
|
error: function() { |
|
1491
|
|
|
applyAll(error, this, arguments); |
|
1492
|
|
|
callback(); |
|
1493
|
|
|
}, |
|
1494
|
|
|
complete: function() { |
|
1495
|
|
|
applyAll(complete, this, arguments); |
|
1496
|
|
|
popLoading(); |
|
1497
|
|
|
} |
|
1498
|
|
|
})); |
|
1499
|
|
|
}else{ |
|
1500
|
|
|
callback(); |
|
1501
|
|
|
} |
|
1502
|
|
|
} |
|
1503
|
|
|
} |
|
1504
|
|
|
|
|
1505
|
|
|
|
|
1506
|
|
|
|
|
1507
|
|
|
/* Sources |
|
1508
|
|
|
-----------------------------------------------------------------------------*/ |
|
1509
|
|
|
|
|
1510
|
|
|
|
|
1511
|
|
|
function addEventSource(sourceInput) { |
|
1512
|
|
|
var source = buildEventSource(sourceInput); |
|
1513
|
|
|
if (source) { |
|
1514
|
|
|
sources.push(source); |
|
1515
|
|
|
pendingSourceCnt++; |
|
1516
|
|
|
fetchEventSource(source, currentFetchID); // will eventually call reportEvents |
|
1517
|
|
|
} |
|
1518
|
|
|
} |
|
1519
|
|
|
|
|
1520
|
|
|
|
|
1521
|
|
|
function buildEventSource(sourceInput) { // will return undefined if invalid source |
|
1522
|
|
|
var normalizers = fc.sourceNormalizers; |
|
1523
|
|
|
var source; |
|
1524
|
|
|
var i; |
|
1525
|
|
|
|
|
1526
|
|
|
if ($.isFunction(sourceInput) || $.isArray(sourceInput)) { |
|
1527
|
|
|
source = { events: sourceInput }; |
|
1528
|
|
|
} |
|
1529
|
|
|
else if (typeof sourceInput === 'string') { |
|
1530
|
|
|
source = { url: sourceInput }; |
|
1531
|
|
|
} |
|
1532
|
|
|
else if (typeof sourceInput === 'object') { |
|
1533
|
|
|
source = $.extend({}, sourceInput); // shallow copy |
|
1534
|
|
|
} |
|
1535
|
|
|
|
|
1536
|
|
|
if (source) { |
|
1537
|
|
|
|
|
1538
|
|
|
// TODO: repeat code, same code for event classNames |
|
1539
|
|
|
if (source.className) { |
|
1540
|
|
|
if (typeof source.className === 'string') { |
|
1541
|
|
|
source.className = source.className.split(/\s+/); |
|
1542
|
|
|
} |
|
1543
|
|
|
// otherwise, assumed to be an array |
|
1544
|
|
|
} |
|
1545
|
|
|
else { |
|
1546
|
|
|
source.className = []; |
|
1547
|
|
|
} |
|
1548
|
|
|
|
|
1549
|
|
|
// for array sources, we convert to standard Event Objects up front |
|
1550
|
|
|
if ($.isArray(source.events)) { |
|
1551
|
|
|
source.origArray = source.events; // for removeEventSource |
|
1552
|
|
|
source.events = $.map(source.events, function(eventInput) { |
|
1553
|
|
|
return buildEvent(eventInput, source); |
|
1554
|
|
|
}); |
|
1555
|
|
|
} |
|
1556
|
|
|
|
|
1557
|
|
|
for (i=0; i<normalizers.length; i++) { |
|
1558
|
|
|
normalizers[i].call(t, source); |
|
1559
|
|
|
} |
|
1560
|
|
|
|
|
1561
|
|
|
return source; |
|
1562
|
|
|
} |
|
1563
|
|
|
} |
|
1564
|
|
|
|
|
1565
|
|
|
|
|
1566
|
|
|
function removeEventSource(source) { |
|
1567
|
|
|
sources = $.grep(sources, function(src) { |
|
1568
|
|
|
return !isSourcesEqual(src, source); |
|
1569
|
|
|
}); |
|
1570
|
|
|
// remove all client events from that source |
|
1571
|
|
|
cache = $.grep(cache, function(e) { |
|
1572
|
|
|
return !isSourcesEqual(e.source, source); |
|
1573
|
|
|
}); |
|
1574
|
|
|
reportEvents(cache); |
|
1575
|
|
|
} |
|
1576
|
|
|
|
|
1577
|
|
|
|
|
1578
|
|
|
function isSourcesEqual(source1, source2) { |
|
1579
|
|
|
return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2); |
|
1580
|
|
|
} |
|
1581
|
|
|
|
|
1582
|
|
|
|
|
1583
|
|
|
function getSourcePrimitive(source) { |
|
1584
|
|
|
return ( |
|
1585
|
|
|
(typeof source === 'object') ? // a normalized event source? |
|
1586
|
|
|
(source.origArray || source.url || source.events) : // get the primitive |
|
1587
|
|
|
null |
|
1588
|
|
|
) || |
|
1589
|
|
|
source; // the given argument *is* the primitive |
|
1590
|
|
|
} |
|
1591
|
|
|
|
|
1592
|
|
|
|
|
1593
|
|
|
|
|
1594
|
|
|
/* Manipulation |
|
1595
|
|
|
-----------------------------------------------------------------------------*/ |
|
1596
|
|
|
|
|
1597
|
|
|
|
|
1598
|
|
|
function updateEvent(event) { |
|
1599
|
|
|
|
|
1600
|
|
|
event.start = t.moment(event.start); |
|
1601
|
|
|
if (event.end) { |
|
1602
|
|
|
event.end = t.moment(event.end); |
|
1603
|
|
|
} |
|
1604
|
|
|
|
|
1605
|
|
|
mutateEvent(event); |
|
1606
|
|
|
propagateMiscProperties(event); |
|
1607
|
|
|
reportEvents(cache); // reports event modifications (so we can redraw) |
|
1608
|
|
|
} |
|
1609
|
|
|
|
|
1610
|
|
|
|
|
1611
|
|
|
var miscCopyableProps = [ |
|
1612
|
|
|
'title', |
|
1613
|
|
|
'url', |
|
1614
|
|
|
'allDay', |
|
1615
|
|
|
'className', |
|
1616
|
|
|
'editable', |
|
1617
|
|
|
'color', |
|
1618
|
|
|
'backgroundColor', |
|
1619
|
|
|
'borderColor', |
|
1620
|
|
|
'textColor' |
|
1621
|
|
|
]; |
|
1622
|
|
|
|
|
1623
|
|
|
function propagateMiscProperties(event) { |
|
1624
|
|
|
var i; |
|
1625
|
|
|
var cachedEvent; |
|
1626
|
|
|
var j; |
|
1627
|
|
|
var prop; |
|
1628
|
|
|
|
|
1629
|
|
|
for (i=0; i<cache.length; i++) { |
|
1630
|
|
|
cachedEvent = cache[i]; |
|
1631
|
|
|
if (cachedEvent._id == event._id && cachedEvent !== event) { |
|
1632
|
|
|
for (j=0; j<miscCopyableProps.length; j++) { |
|
1633
|
|
|
prop = miscCopyableProps[j]; |
|
1634
|
|
|
if (event[prop] !== undefined) { |
|
1635
|
|
|
cachedEvent[prop] = event[prop]; |
|
1636
|
|
|
} |
|
1637
|
|
|
} |
|
1638
|
|
|
} |
|
1639
|
|
|
} |
|
1640
|
|
|
} |
|
1641
|
|
|
|
|
1642
|
|
|
|
|
1643
|
|
|
|
|
1644
|
|
|
function renderEvent(eventData, stick) { |
|
1645
|
|
|
var event = buildEvent(eventData); |
|
1646
|
|
|
if (event) { |
|
1647
|
|
|
if (!event.source) { |
|
1648
|
|
|
if (stick) { |
|
1649
|
|
|
stickySource.events.push(event); |
|
1650
|
|
|
event.source = stickySource; |
|
1651
|
|
|
} |
|
1652
|
|
|
cache.push(event); |
|
1653
|
|
|
} |
|
1654
|
|
|
reportEvents(cache); |
|
1655
|
|
|
} |
|
1656
|
|
|
} |
|
1657
|
|
|
|
|
1658
|
|
|
|
|
1659
|
|
|
function removeEvents(filter) { |
|
1660
|
|
|
var eventID; |
|
1661
|
|
|
var i; |
|
1662
|
|
|
|
|
1663
|
|
|
if (filter == null) { // null or undefined. remove all events |
|
1664
|
|
|
filter = function() { return true; }; // will always match |
|
1665
|
|
|
} |
|
1666
|
|
|
else if (!$.isFunction(filter)) { // an event ID |
|
1667
|
|
|
eventID = filter + ''; |
|
1668
|
|
|
filter = function(event) { |
|
1669
|
|
|
return event._id == eventID; |
|
1670
|
|
|
}; |
|
1671
|
|
|
} |
|
1672
|
|
|
|
|
1673
|
|
|
// Purge event(s) from our local cache |
|
1674
|
|
|
cache = $.grep(cache, filter, true); // inverse=true |
|
1675
|
|
|
|
|
1676
|
|
|
// Remove events from array sources. |
|
1677
|
|
|
// This works because they have been converted to official Event Objects up front. |
|
1678
|
|
|
// (and as a result, event._id has been calculated). |
|
1679
|
|
|
for (i=0; i<sources.length; i++) { |
|
1680
|
|
|
if ($.isArray(sources[i].events)) { |
|
1681
|
|
|
sources[i].events = $.grep(sources[i].events, filter, true); |
|
1682
|
|
|
} |
|
1683
|
|
|
} |
|
1684
|
|
|
|
|
1685
|
|
|
reportEvents(cache); |
|
1686
|
|
|
} |
|
1687
|
|
|
|
|
1688
|
|
|
|
|
1689
|
|
|
function clientEvents(filter) { |
|
1690
|
|
|
if ($.isFunction(filter)) { |
|
1691
|
|
|
return $.grep(cache, filter); |
|
1692
|
|
|
} |
|
1693
|
|
|
else if (filter != null) { // not null, not undefined. an event ID |
|
1694
|
|
|
filter += ''; |
|
1695
|
|
|
return $.grep(cache, function(e) { |
|
1696
|
|
|
return e._id == filter; |
|
1697
|
|
|
}); |
|
1698
|
|
|
} |
|
1699
|
|
|
return cache; // else, return all |
|
1700
|
|
|
} |
|
1701
|
|
|
|
|
1702
|
|
|
|
|
1703
|
|
|
|
|
1704
|
|
|
/* Loading State |
|
1705
|
|
|
-----------------------------------------------------------------------------*/ |
|
1706
|
|
|
|
|
1707
|
|
|
|
|
1708
|
|
|
function pushLoading() { |
|
1709
|
|
|
if (!(loadingLevel++)) { |
|
1710
|
|
|
trigger('loading', null, true, getView()); |
|
1711
|
|
|
} |
|
1712
|
|
|
} |
|
1713
|
|
|
|
|
1714
|
|
|
|
|
1715
|
|
|
function popLoading() { |
|
1716
|
|
|
if (!(--loadingLevel)) { |
|
1717
|
|
|
trigger('loading', null, false, getView()); |
|
1718
|
|
|
} |
|
1719
|
|
|
} |
|
1720
|
|
|
|
|
1721
|
|
|
|
|
1722
|
|
|
|
|
1723
|
|
|
/* Event Normalization |
|
1724
|
|
|
-----------------------------------------------------------------------------*/ |
|
1725
|
|
|
|
|
1726
|
|
|
function buildEvent(data, source) { // source may be undefined! |
|
1727
|
|
|
var out = {}; |
|
1728
|
|
|
var start; |
|
1729
|
|
|
var end; |
|
1730
|
|
|
var allDay; |
|
1731
|
|
|
var allDayDefault; |
|
1732
|
|
|
|
|
1733
|
|
|
if (options.eventDataTransform) { |
|
1734
|
|
|
data = options.eventDataTransform(data); |
|
1735
|
|
|
} |
|
1736
|
|
|
if (source && source.eventDataTransform) { |
|
1737
|
|
|
data = source.eventDataTransform(data); |
|
1738
|
|
|
} |
|
1739
|
|
|
|
|
1740
|
|
|
start = t.moment(data.start || data.date); // "date" is an alias for "start" |
|
1741
|
|
|
if (!start.isValid()) { |
|
1742
|
|
|
return; |
|
|
|
|
|
|
1743
|
|
|
} |
|
1744
|
|
|
|
|
1745
|
|
|
end = null; |
|
1746
|
|
|
if (data.end) { |
|
1747
|
|
|
end = t.moment(data.end); |
|
1748
|
|
|
if (!end.isValid()) { |
|
1749
|
|
|
return; |
|
|
|
|
|
|
1750
|
|
|
} |
|
1751
|
|
|
} |
|
1752
|
|
|
|
|
1753
|
|
|
allDay = data.allDay; |
|
1754
|
|
|
if (allDay === undefined) { |
|
1755
|
|
|
allDayDefault = firstDefined( |
|
1756
|
|
|
source ? source.allDayDefault : undefined, |
|
1757
|
|
|
options.allDayDefault |
|
1758
|
|
|
); |
|
1759
|
|
|
if (allDayDefault !== undefined) { |
|
1760
|
|
|
// use the default |
|
1761
|
|
|
allDay = allDayDefault; |
|
1762
|
|
|
} |
|
1763
|
|
|
else { |
|
1764
|
|
|
// all dates need to have ambig time for the event to be considered allDay |
|
1765
|
|
|
allDay = !start.hasTime() && (!end || !end.hasTime()); |
|
1766
|
|
|
} |
|
1767
|
|
|
} |
|
1768
|
|
|
|
|
1769
|
|
|
// normalize the date based on allDay |
|
1770
|
|
|
if (allDay) { |
|
1771
|
|
|
// neither date should have a time |
|
1772
|
|
|
if (start.hasTime()) { |
|
1773
|
|
|
start.stripTime(); |
|
1774
|
|
|
} |
|
1775
|
|
|
if (end && end.hasTime()) { |
|
1776
|
|
|
end.stripTime(); |
|
1777
|
|
|
} |
|
1778
|
|
|
} |
|
1779
|
|
|
else { |
|
1780
|
|
|
// force a time/zone up the dates |
|
1781
|
|
|
if (!start.hasTime()) { |
|
1782
|
|
|
start = t.rezoneDate(start); |
|
1783
|
|
|
} |
|
1784
|
|
|
if (end && !end.hasTime()) { |
|
1785
|
|
|
end = t.rezoneDate(end); |
|
1786
|
|
|
} |
|
1787
|
|
|
} |
|
1788
|
|
|
|
|
1789
|
|
|
// Copy all properties over to the resulting object. |
|
1790
|
|
|
// The special-case properties will be copied over afterwards. |
|
1791
|
|
|
$.extend(out, data); |
|
1792
|
|
|
|
|
1793
|
|
|
if (source) { |
|
1794
|
|
|
out.source = source; |
|
1795
|
|
|
} |
|
1796
|
|
|
|
|
1797
|
|
|
out._id = data._id || (data.id === undefined ? '_fc' + eventGUID++ : data.id + ''); |
|
1798
|
|
|
|
|
1799
|
|
|
if (data.className) { |
|
1800
|
|
|
if (typeof data.className == 'string') { |
|
1801
|
|
|
out.className = data.className.split(/\s+/); |
|
1802
|
|
|
} |
|
1803
|
|
|
else { // assumed to be an array |
|
1804
|
|
|
out.className = data.className; |
|
1805
|
|
|
} |
|
1806
|
|
|
} |
|
1807
|
|
|
else { |
|
1808
|
|
|
out.className = []; |
|
1809
|
|
|
} |
|
1810
|
|
|
|
|
1811
|
|
|
out.allDay = allDay; |
|
1812
|
|
|
out.start = start; |
|
1813
|
|
|
out.end = end; |
|
1814
|
|
|
|
|
1815
|
|
|
if (options.forceEventDuration && !out.end) { |
|
1816
|
|
|
out.end = getEventEnd(out); |
|
1817
|
|
|
} |
|
1818
|
|
|
|
|
1819
|
|
|
backupEventDates(out); |
|
1820
|
|
|
|
|
1821
|
|
|
return out; |
|
1822
|
|
|
} |
|
1823
|
|
|
|
|
1824
|
|
|
|
|
1825
|
|
|
|
|
1826
|
|
|
/* Event Modification Math |
|
1827
|
|
|
-----------------------------------------------------------------------------------------*/ |
|
1828
|
|
|
|
|
1829
|
|
|
|
|
1830
|
|
|
// Modify the date(s) of an event and make this change propagate to all other events with |
|
1831
|
|
|
// the same ID (related repeating events). |
|
1832
|
|
|
// |
|
1833
|
|
|
// If `newStart`/`newEnd` are not specified, the "new" dates are assumed to be `event.start` and `event.end`. |
|
1834
|
|
|
// The "old" dates to be compare against are always `event._start` and `event._end` (set by EventManager). |
|
1835
|
|
|
// |
|
1836
|
|
|
// Returns an object with delta information and a function to undo all operations. |
|
1837
|
|
|
// |
|
1838
|
|
|
function mutateEvent(event, newStart, newEnd) { |
|
1839
|
|
|
var oldAllDay = event._allDay; |
|
1840
|
|
|
var oldStart = event._start; |
|
1841
|
|
|
var oldEnd = event._end; |
|
1842
|
|
|
var clearEnd = false; |
|
1843
|
|
|
var newAllDay; |
|
1844
|
|
|
var dateDelta; |
|
1845
|
|
|
var durationDelta; |
|
1846
|
|
|
var undoFunc; |
|
1847
|
|
|
|
|
1848
|
|
|
// if no new dates were passed in, compare against the event's existing dates |
|
1849
|
|
|
if (!newStart && !newEnd) { |
|
1850
|
|
|
newStart = event.start; |
|
1851
|
|
|
newEnd = event.end; |
|
1852
|
|
|
} |
|
1853
|
|
|
|
|
1854
|
|
|
// NOTE: throughout this function, the initial values of `newStart` and `newEnd` are |
|
1855
|
|
|
// preserved. These values may be undefined. |
|
1856
|
|
|
|
|
1857
|
|
|
// detect new allDay |
|
1858
|
|
|
if (event.allDay != oldAllDay) { // if value has changed, use it |
|
1859
|
|
|
newAllDay = event.allDay; |
|
1860
|
|
|
} |
|
1861
|
|
|
else { // otherwise, see if any of the new dates are allDay |
|
1862
|
|
|
newAllDay = !(newStart || newEnd).hasTime(); |
|
1863
|
|
|
} |
|
1864
|
|
|
|
|
1865
|
|
|
// normalize the new dates based on allDay |
|
1866
|
|
|
if (newAllDay) { |
|
1867
|
|
|
if (newStart) { |
|
1868
|
|
|
newStart = newStart.clone().stripTime(); |
|
1869
|
|
|
} |
|
1870
|
|
|
if (newEnd) { |
|
1871
|
|
|
newEnd = newEnd.clone().stripTime(); |
|
1872
|
|
|
} |
|
1873
|
|
|
} |
|
1874
|
|
|
|
|
1875
|
|
|
// compute dateDelta |
|
1876
|
|
|
if (newStart) { |
|
1877
|
|
|
if (newAllDay) { |
|
1878
|
|
|
dateDelta = dayishDiff(newStart, oldStart.clone().stripTime()); // treat oldStart as allDay |
|
1879
|
|
|
} |
|
1880
|
|
|
else { |
|
1881
|
|
|
dateDelta = dayishDiff(newStart, oldStart); |
|
1882
|
|
|
} |
|
1883
|
|
|
} |
|
1884
|
|
|
|
|
1885
|
|
|
if (newAllDay != oldAllDay) { |
|
1886
|
|
|
// if allDay has changed, always throw away the end |
|
1887
|
|
|
clearEnd = true; |
|
1888
|
|
|
} |
|
1889
|
|
|
else if (newEnd) { |
|
1890
|
|
|
durationDelta = dayishDiff( |
|
1891
|
|
|
// new duration |
|
1892
|
|
|
newEnd || t.getDefaultEventEnd(newAllDay, newStart || oldStart), |
|
1893
|
|
|
newStart || oldStart |
|
1894
|
|
|
).subtract(dayishDiff( |
|
1895
|
|
|
// subtract old duration |
|
1896
|
|
|
oldEnd || t.getDefaultEventEnd(oldAllDay, oldStart), |
|
1897
|
|
|
oldStart |
|
1898
|
|
|
)); |
|
1899
|
|
|
} |
|
1900
|
|
|
|
|
1901
|
|
|
undoFunc = mutateEvents( |
|
1902
|
|
|
clientEvents(event._id), // get events with this ID |
|
1903
|
|
|
clearEnd, |
|
1904
|
|
|
newAllDay, |
|
1905
|
|
|
dateDelta, |
|
|
|
|
|
|
1906
|
|
|
durationDelta |
|
|
|
|
|
|
1907
|
|
|
); |
|
1908
|
|
|
|
|
1909
|
|
|
return { |
|
1910
|
|
|
dateDelta: dateDelta, |
|
1911
|
|
|
durationDelta: durationDelta, |
|
1912
|
|
|
undo: undoFunc |
|
1913
|
|
|
}; |
|
1914
|
|
|
} |
|
1915
|
|
|
|
|
1916
|
|
|
|
|
1917
|
|
|
// Modifies an array of events in the following ways (operations are in order): |
|
1918
|
|
|
// - clear the event's `end` |
|
1919
|
|
|
// - convert the event to allDay |
|
1920
|
|
|
// - add `dateDelta` to the start and end |
|
1921
|
|
|
// - add `durationDelta` to the event's duration |
|
1922
|
|
|
// |
|
1923
|
|
|
// Returns a function that can be called to undo all the operations. |
|
1924
|
|
|
// |
|
1925
|
|
|
function mutateEvents(events, clearEnd, forceAllDay, dateDelta, durationDelta) { |
|
1926
|
|
|
var isAmbigTimezone = t.getIsAmbigTimezone(); |
|
1927
|
|
|
var undoFunctions = []; |
|
1928
|
|
|
|
|
1929
|
|
|
$.each(events, function(i, event) { |
|
1930
|
|
|
var oldAllDay = event._allDay; |
|
1931
|
|
|
var oldStart = event._start; |
|
1932
|
|
|
var oldEnd = event._end; |
|
1933
|
|
|
var newAllDay = forceAllDay != null ? forceAllDay : oldAllDay; |
|
1934
|
|
|
var newStart = oldStart.clone(); |
|
1935
|
|
|
var newEnd = (!clearEnd && oldEnd) ? oldEnd.clone() : null; |
|
1936
|
|
|
|
|
1937
|
|
|
// NOTE: this function is responsible for transforming `newStart` and `newEnd`, |
|
1938
|
|
|
// which were initialized to the OLD values first. `newEnd` may be null. |
|
1939
|
|
|
|
|
1940
|
|
|
// normlize newStart/newEnd to be consistent with newAllDay |
|
1941
|
|
|
if (newAllDay) { |
|
1942
|
|
|
newStart.stripTime(); |
|
1943
|
|
|
if (newEnd) { |
|
1944
|
|
|
newEnd.stripTime(); |
|
1945
|
|
|
} |
|
1946
|
|
|
} |
|
1947
|
|
|
else { |
|
1948
|
|
|
if (!newStart.hasTime()) { |
|
1949
|
|
|
newStart = t.rezoneDate(newStart); |
|
1950
|
|
|
} |
|
1951
|
|
|
if (newEnd && !newEnd.hasTime()) { |
|
1952
|
|
|
newEnd = t.rezoneDate(newEnd); |
|
1953
|
|
|
} |
|
1954
|
|
|
} |
|
1955
|
|
|
|
|
1956
|
|
|
// ensure we have an end date if necessary |
|
1957
|
|
|
if (!newEnd && (options.forceEventDuration || +durationDelta)) { |
|
1958
|
|
|
newEnd = t.getDefaultEventEnd(newAllDay, newStart); |
|
1959
|
|
|
} |
|
1960
|
|
|
|
|
1961
|
|
|
// translate the dates |
|
1962
|
|
|
newStart.add(dateDelta); |
|
1963
|
|
|
if (newEnd) { |
|
1964
|
|
|
newEnd.add(dateDelta).add(durationDelta); |
|
1965
|
|
|
} |
|
1966
|
|
|
|
|
1967
|
|
|
// if the dates have changed, and we know it is impossible to recompute the |
|
1968
|
|
|
// timezone offsets, strip the zone. |
|
1969
|
|
|
if (isAmbigTimezone) { |
|
1970
|
|
|
if (+dateDelta || +durationDelta) { |
|
1971
|
|
|
newStart.stripZone(); |
|
1972
|
|
|
if (newEnd) { |
|
1973
|
|
|
newEnd.stripZone(); |
|
1974
|
|
|
} |
|
1975
|
|
|
} |
|
1976
|
|
|
} |
|
1977
|
|
|
|
|
1978
|
|
|
event.allDay = newAllDay; |
|
1979
|
|
|
event.start = newStart; |
|
1980
|
|
|
event.end = newEnd; |
|
1981
|
|
|
backupEventDates(event); |
|
1982
|
|
|
|
|
1983
|
|
|
undoFunctions.push(function() { |
|
1984
|
|
|
event.allDay = oldAllDay; |
|
1985
|
|
|
event.start = oldStart; |
|
1986
|
|
|
event.end = oldEnd; |
|
1987
|
|
|
backupEventDates(event); |
|
1988
|
|
|
}); |
|
1989
|
|
|
}); |
|
1990
|
|
|
|
|
1991
|
|
|
return function() { |
|
1992
|
|
|
for (var i=0; i<undoFunctions.length; i++) { |
|
1993
|
|
|
undoFunctions[i](); |
|
1994
|
|
|
} |
|
1995
|
|
|
}; |
|
1996
|
|
|
} |
|
1997
|
|
|
|
|
1998
|
|
|
} |
|
1999
|
|
|
|
|
2000
|
|
|
|
|
2001
|
|
|
// updates the "backup" properties, which are preserved in order to compute diffs later on. |
|
2002
|
|
|
function backupEventDates(event) { |
|
2003
|
|
|
event._allDay = event.allDay; |
|
2004
|
|
|
event._start = event.start.clone(); |
|
2005
|
|
|
event._end = event.end ? event.end.clone() : null; |
|
2006
|
|
|
} |
|
2007
|
|
|
|
|
2008
|
|
|
;; |
|
2009
|
|
|
|
|
2010
|
|
|
/* FullCalendar-specific DOM Utilities |
|
2011
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
|
2012
|
|
|
|
|
2013
|
|
|
|
|
2014
|
|
|
// Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left |
|
2015
|
|
|
// and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that. |
|
2016
|
|
|
function compensateScroll(rowEls, scrollbarWidths) { |
|
2017
|
|
|
if (scrollbarWidths.left) { |
|
2018
|
|
|
rowEls.css({ |
|
2019
|
|
|
'border-left-width': 1, |
|
2020
|
|
|
'margin-left': scrollbarWidths.left - 1 |
|
2021
|
|
|
}); |
|
2022
|
|
|
} |
|
2023
|
|
|
if (scrollbarWidths.right) { |
|
2024
|
|
|
rowEls.css({ |
|
2025
|
|
|
'border-right-width': 1, |
|
2026
|
|
|
'margin-right': scrollbarWidths.right - 1 |
|
2027
|
|
|
}); |
|
2028
|
|
|
} |
|
2029
|
|
|
} |
|
2030
|
|
|
|
|
2031
|
|
|
|
|
2032
|
|
|
// Undoes compensateScroll and restores all borders/margins |
|
2033
|
|
|
function uncompensateScroll(rowEls) { |
|
2034
|
|
|
rowEls.css({ |
|
2035
|
|
|
'margin-left': '', |
|
2036
|
|
|
'margin-right': '', |
|
2037
|
|
|
'border-left-width': '', |
|
2038
|
|
|
'border-right-width': '' |
|
2039
|
|
|
}); |
|
2040
|
|
|
} |
|
2041
|
|
|
|
|
2042
|
|
|
|
|
2043
|
|
|
// Given a total available height to fill, have `els` (essentially child rows) expand to accomodate. |
|
2044
|
|
|
// By default, all elements that are shorter than the recommended height are expanded uniformly, not considering |
|
2045
|
|
|
// any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and |
|
2046
|
|
|
// reduces the available height. |
|
2047
|
|
|
function distributeHeight(els, availableHeight, shouldRedistribute) { |
|
2048
|
|
|
|
|
2049
|
|
|
// *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions, |
|
2050
|
|
|
// and it is better to be shorter than taller, to avoid creating unnecessary scrollbars. |
|
2051
|
|
|
|
|
2052
|
|
|
var minOffset1 = Math.floor(availableHeight / els.length); // for non-last element |
|
2053
|
|
|
var minOffset2 = Math.floor(availableHeight - minOffset1 * (els.length - 1)); // for last element *FLOORING NOTE* |
|
2054
|
|
|
var flexEls = []; // elements that are allowed to expand. array of DOM nodes |
|
2055
|
|
|
var flexOffsets = []; // amount of vertical space it takes up |
|
2056
|
|
|
var flexHeights = []; // actual css height |
|
2057
|
|
|
var usedHeight = 0; |
|
2058
|
|
|
|
|
2059
|
|
|
undistributeHeight(els); // give all elements their natural height |
|
2060
|
|
|
|
|
2061
|
|
|
// find elements that are below the recommended height (expandable). |
|
2062
|
|
|
// important to query for heights in a single first pass (to avoid reflow oscillation). |
|
2063
|
|
|
els.each(function(i, el) { |
|
2064
|
|
|
var minOffset = i === els.length - 1 ? minOffset2 : minOffset1; |
|
2065
|
|
|
var naturalOffset = $(el).outerHeight(true); |
|
2066
|
|
|
|
|
2067
|
|
|
if (naturalOffset < minOffset) { |
|
2068
|
|
|
flexEls.push(el); |
|
2069
|
|
|
flexOffsets.push(naturalOffset); |
|
2070
|
|
|
flexHeights.push($(el).height()); |
|
2071
|
|
|
} |
|
2072
|
|
|
else { |
|
2073
|
|
|
// this element stretches past recommended height (non-expandable). mark the space as occupied. |
|
2074
|
|
|
usedHeight += naturalOffset; |
|
2075
|
|
|
} |
|
2076
|
|
|
}); |
|
2077
|
|
|
|
|
2078
|
|
|
// readjust the recommended height to only consider the height available to non-maxed-out rows. |
|
2079
|
|
|
if (shouldRedistribute) { |
|
2080
|
|
|
availableHeight -= usedHeight; |
|
2081
|
|
|
minOffset1 = Math.floor(availableHeight / flexEls.length); |
|
2082
|
|
|
minOffset2 = Math.floor(availableHeight - minOffset1 * (flexEls.length - 1)); // *FLOORING NOTE* |
|
2083
|
|
|
} |
|
2084
|
|
|
|
|
2085
|
|
|
// assign heights to all expandable elements |
|
2086
|
|
|
$(flexEls).each(function(i, el) { |
|
2087
|
|
|
var minOffset = i === flexEls.length - 1 ? minOffset2 : minOffset1; |
|
2088
|
|
|
var naturalOffset = flexOffsets[i]; |
|
2089
|
|
|
var naturalHeight = flexHeights[i]; |
|
2090
|
|
|
var newHeight = minOffset - (naturalOffset - naturalHeight); // subtract the margin/padding |
|
2091
|
|
|
|
|
2092
|
|
|
if (naturalOffset < minOffset) { // we check this again because redistribution might have changed things |
|
2093
|
|
|
$(el).height(newHeight); |
|
2094
|
|
|
} |
|
2095
|
|
|
}); |
|
2096
|
|
|
} |
|
2097
|
|
|
|
|
2098
|
|
|
|
|
2099
|
|
|
// Undoes distrubuteHeight, restoring all els to their natural height |
|
2100
|
|
|
function undistributeHeight(els) { |
|
2101
|
|
|
els.height(''); |
|
2102
|
|
|
} |
|
2103
|
|
|
|
|
2104
|
|
|
|
|
2105
|
|
|
// Given `els`, a jQuery set of <td> cells, find the cell with the largest natural width and set the widths of all the |
|
2106
|
|
|
// cells to be that width. |
|
2107
|
|
|
// PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline |
|
2108
|
|
|
function matchCellWidths(els) { |
|
2109
|
|
|
var maxInnerWidth = 0; |
|
2110
|
|
|
|
|
2111
|
|
|
els.find('> *').each(function(i, innerEl) { |
|
2112
|
|
|
var innerWidth = $(innerEl).outerWidth(); |
|
2113
|
|
|
if (innerWidth > maxInnerWidth) { |
|
2114
|
|
|
maxInnerWidth = innerWidth; |
|
2115
|
|
|
} |
|
2116
|
|
|
}); |
|
2117
|
|
|
|
|
2118
|
|
|
maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance |
|
2119
|
|
|
|
|
2120
|
|
|
els.width(maxInnerWidth); |
|
2121
|
|
|
|
|
2122
|
|
|
return maxInnerWidth; |
|
2123
|
|
|
} |
|
2124
|
|
|
|
|
2125
|
|
|
|
|
2126
|
|
|
// Turns a container element into a scroller if its contents is taller than the allotted height. |
|
2127
|
|
|
// Returns true if the element is now a scroller, false otherwise. |
|
2128
|
|
|
// NOTE: this method is best because it takes weird zooming dimensions into account |
|
2129
|
|
|
function setPotentialScroller(containerEl, height) { |
|
2130
|
|
|
containerEl.height(height).addClass('fc-scroller'); |
|
2131
|
|
|
|
|
2132
|
|
|
// are scrollbars needed? |
|
2133
|
|
|
if (containerEl[0].scrollHeight - 1 > containerEl[0].clientHeight) { // !!! -1 because IE is often off-by-one :( |
|
2134
|
|
|
return true; |
|
2135
|
|
|
} |
|
2136
|
|
|
|
|
2137
|
|
|
unsetScroller(containerEl); // undo |
|
2138
|
|
|
return false; |
|
2139
|
|
|
} |
|
2140
|
|
|
|
|
2141
|
|
|
|
|
2142
|
|
|
// Takes an element that might have been a scroller, and turns it back into a normal element. |
|
2143
|
|
|
function unsetScroller(containerEl) { |
|
2144
|
|
|
containerEl.height('').removeClass('fc-scroller'); |
|
2145
|
|
|
} |
|
2146
|
|
|
|
|
2147
|
|
|
|
|
2148
|
|
|
/* General DOM Utilities |
|
2149
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
|
2150
|
|
|
|
|
2151
|
|
|
|
|
2152
|
|
|
// borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51 |
|
2153
|
|
|
function getScrollParent(el) { |
|
2154
|
|
|
var position = el.css('position'), |
|
2155
|
|
|
scrollParent = el.parents().filter(function() { |
|
2156
|
|
|
var parent = $(this); |
|
2157
|
|
|
return (/(auto|scroll)/).test( |
|
2158
|
|
|
parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x') |
|
2159
|
|
|
); |
|
2160
|
|
|
}).eq(0); |
|
2161
|
|
|
|
|
2162
|
|
|
return position === 'fixed' || !scrollParent.length ? $(el[0].ownerDocument || document) : scrollParent; |
|
2163
|
|
|
} |
|
2164
|
|
|
|
|
2165
|
|
|
|
|
2166
|
|
|
// Given a container element, return an object with the pixel values of the left/right scrollbars. |
|
2167
|
|
|
// Left scrollbars might occur on RTL browsers (IE maybe?) but I have not tested. |
|
2168
|
|
|
// PREREQUISITE: container element must have a single child with display:block |
|
2169
|
|
|
function getScrollbarWidths(container) { |
|
2170
|
|
|
var containerLeft = container.offset().left; |
|
2171
|
|
|
var containerRight = containerLeft + container.width(); |
|
2172
|
|
|
var inner = container.children(); |
|
2173
|
|
|
var innerLeft = inner.offset().left; |
|
2174
|
|
|
var innerRight = innerLeft + inner.outerWidth(); |
|
2175
|
|
|
|
|
2176
|
|
|
return { |
|
2177
|
|
|
left: innerLeft - containerLeft, |
|
2178
|
|
|
right: containerRight - innerRight |
|
2179
|
|
|
}; |
|
2180
|
|
|
} |
|
2181
|
|
|
|
|
2182
|
|
|
|
|
2183
|
|
|
// Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac) |
|
2184
|
|
|
function isPrimaryMouseButton(ev) { |
|
2185
|
|
|
return ev.which == 1 && !ev.ctrlKey; |
|
2186
|
|
|
} |
|
2187
|
|
|
|
|
2188
|
|
|
|
|
2189
|
|
|
/* FullCalendar-specific Misc Utilities |
|
2190
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
|
2191
|
|
|
|
|
2192
|
|
|
|
|
2193
|
|
|
// Creates a basic segment with the intersection of the two ranges. Returns undefined if no intersection. |
|
2194
|
|
|
// Expects all dates to be normalized to the same timezone beforehand. |
|
2195
|
|
|
function intersectionToSeg(subjectStart, subjectEnd, intervalStart, intervalEnd) { |
|
2196
|
|
|
var segStart, segEnd; |
|
2197
|
|
|
var isStart, isEnd; |
|
2198
|
|
|
|
|
2199
|
|
|
if (subjectEnd > intervalStart && subjectStart < intervalEnd) { // in bounds at all? |
|
2200
|
|
|
|
|
2201
|
|
|
if (subjectStart >= intervalStart) { |
|
2202
|
|
|
segStart = subjectStart.clone(); |
|
2203
|
|
|
isStart = true; |
|
2204
|
|
|
} |
|
2205
|
|
|
else { |
|
2206
|
|
|
segStart = intervalStart.clone(); |
|
2207
|
|
|
isStart = false; |
|
2208
|
|
|
} |
|
2209
|
|
|
|
|
2210
|
|
|
if (subjectEnd <= intervalEnd) { |
|
2211
|
|
|
segEnd = subjectEnd.clone(); |
|
2212
|
|
|
isEnd = true; |
|
2213
|
|
|
} |
|
2214
|
|
|
else { |
|
2215
|
|
|
segEnd = intervalEnd.clone(); |
|
2216
|
|
|
isEnd = false; |
|
2217
|
|
|
} |
|
2218
|
|
|
|
|
2219
|
|
|
return { |
|
2220
|
|
|
start: segStart, |
|
2221
|
|
|
end: segEnd, |
|
2222
|
|
|
isStart: isStart, |
|
2223
|
|
|
isEnd: isEnd |
|
2224
|
|
|
}; |
|
2225
|
|
|
} |
|
2226
|
|
|
} |
|
2227
|
|
|
|
|
2228
|
|
|
|
|
2229
|
|
|
function smartProperty(obj, name) { // get a camel-cased/namespaced property of an object |
|
2230
|
|
|
obj = obj || {}; |
|
2231
|
|
|
if (obj[name] !== undefined) { |
|
2232
|
|
|
return obj[name]; |
|
2233
|
|
|
} |
|
2234
|
|
|
var parts = name.split(/(?=[A-Z])/), |
|
2235
|
|
|
i = parts.length - 1, res; |
|
2236
|
|
|
for (; i>=0; i--) { |
|
2237
|
|
|
res = obj[parts[i].toLowerCase()]; |
|
2238
|
|
|
if (res !== undefined) { |
|
2239
|
|
|
return res; |
|
2240
|
|
|
} |
|
2241
|
|
|
} |
|
2242
|
|
|
return obj['default']; |
|
2243
|
|
|
} |
|
2244
|
|
|
|
|
2245
|
|
|
|
|
2246
|
|
|
/* Date Utilities |
|
2247
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
|
2248
|
|
|
|
|
2249
|
|
|
var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ]; |
|
2250
|
|
|
|
|
2251
|
|
|
|
|
2252
|
|
|
// Diffs the two moments into a Duration where full-days are recorded first, then the remaining time. |
|
2253
|
|
|
// Moments will have their timezones normalized. |
|
2254
|
|
|
function dayishDiff(a, b) { |
|
2255
|
|
|
return moment.duration({ |
|
2256
|
|
|
days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'), |
|
2257
|
|
|
ms: a.time() - b.time() |
|
2258
|
|
|
}); |
|
2259
|
|
|
} |
|
2260
|
|
|
|
|
2261
|
|
|
|
|
2262
|
|
|
function isNativeDate(input) { |
|
2263
|
|
|
return Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date; |
|
2264
|
|
|
} |
|
2265
|
|
|
|
|
2266
|
|
|
|
|
2267
|
|
|
function dateCompare(a, b) { // works with Moments and native Dates |
|
2268
|
|
|
return a - b; |
|
2269
|
|
|
} |
|
2270
|
|
|
|
|
2271
|
|
|
|
|
2272
|
|
|
/* General Utilities |
|
2273
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
|
2274
|
|
|
|
|
2275
|
|
|
fc.applyAll = applyAll; // export |
|
2276
|
|
|
|
|
2277
|
|
|
|
|
2278
|
|
|
// Create an object that has the given prototype. Just like Object.create |
|
2279
|
|
|
function createObject(proto) { |
|
2280
|
|
|
var f = function() {}; |
|
2281
|
|
|
f.prototype = proto; |
|
2282
|
|
|
return new f(); |
|
|
|
|
|
|
2283
|
|
|
} |
|
2284
|
|
|
|
|
2285
|
|
|
|
|
2286
|
|
|
// Copies specifically-owned (non-protoype) properties of `b` onto `a`. |
|
2287
|
|
|
// FYI, $.extend would copy *all* properties of `b` onto `a`. |
|
2288
|
|
|
function extend(a, b) { |
|
2289
|
|
|
for (var i in b) { |
|
2290
|
|
|
if (b.hasOwnProperty(i)) { |
|
2291
|
|
|
a[i] = b[i]; |
|
2292
|
|
|
} |
|
2293
|
|
|
} |
|
2294
|
|
|
} |
|
2295
|
|
|
|
|
2296
|
|
|
|
|
2297
|
|
|
function applyAll(functions, thisObj, args) { |
|
2298
|
|
|
if ($.isFunction(functions)) { |
|
2299
|
|
|
functions = [ functions ]; |
|
2300
|
|
|
} |
|
2301
|
|
|
if (functions) { |
|
2302
|
|
|
var i; |
|
2303
|
|
|
var ret; |
|
2304
|
|
|
for (i=0; i<functions.length; i++) { |
|
2305
|
|
|
ret = functions[i].apply(thisObj, args) || ret; |
|
2306
|
|
|
} |
|
2307
|
|
|
return ret; |
|
|
|
|
|
|
2308
|
|
|
} |
|
2309
|
|
|
} |
|
2310
|
|
|
|
|
2311
|
|
|
|
|
2312
|
|
|
function firstDefined() { |
|
2313
|
|
|
for (var i=0; i<arguments.length; i++) { |
|
2314
|
|
|
if (arguments[i] !== undefined) { |
|
2315
|
|
|
return arguments[i]; |
|
2316
|
|
|
} |
|
2317
|
|
|
} |
|
|
|
|
|
|
2318
|
|
|
} |
|
2319
|
|
|
|
|
2320
|
|
|
|
|
2321
|
|
|
function htmlEscape(s) { |
|
2322
|
|
|
return (s + '').replace(/&/g, '&') |
|
2323
|
|
|
.replace(/</g, '<') |
|
2324
|
|
|
.replace(/>/g, '>') |
|
2325
|
|
|
.replace(/'/g, ''') |
|
2326
|
|
|
.replace(/"/g, '"') |
|
2327
|
|
|
.replace(/\n/g, '<br />'); |
|
2328
|
|
|
} |
|
2329
|
|
|
|
|
2330
|
|
|
|
|
2331
|
|
|
function stripHtmlEntities(text) { |
|
2332
|
|
|
return text.replace(/&.*?;/g, ''); |
|
2333
|
|
|
} |
|
2334
|
|
|
|
|
2335
|
|
|
|
|
2336
|
|
|
function capitaliseFirstLetter(str) { |
|
2337
|
|
|
return str.charAt(0).toUpperCase() + str.slice(1); |
|
2338
|
|
|
} |
|
2339
|
|
|
|
|
2340
|
|
|
|
|
2341
|
|
|
// Returns a function, that, as long as it continues to be invoked, will not |
|
2342
|
|
|
// be triggered. The function will be called after it stops being called for |
|
2343
|
|
|
// N milliseconds. |
|
2344
|
|
|
// https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714 |
|
2345
|
|
|
function debounce(func, wait) { |
|
2346
|
|
|
var timeoutId; |
|
2347
|
|
|
var args; |
|
2348
|
|
|
var context; |
|
2349
|
|
|
var timestamp; // of most recent call |
|
2350
|
|
|
var later = function() { |
|
2351
|
|
|
var last = +new Date() - timestamp; |
|
2352
|
|
|
if (last < wait && last > 0) { |
|
2353
|
|
|
timeoutId = setTimeout(later, wait - last); |
|
2354
|
|
|
} |
|
2355
|
|
|
else { |
|
2356
|
|
|
timeoutId = null; |
|
2357
|
|
|
func.apply(context, args); |
|
2358
|
|
|
if (!timeoutId) { |
|
2359
|
|
|
context = args = null; |
|
2360
|
|
|
} |
|
2361
|
|
|
} |
|
2362
|
|
|
}; |
|
2363
|
|
|
|
|
2364
|
|
|
return function() { |
|
2365
|
|
|
context = this; |
|
2366
|
|
|
args = arguments; |
|
2367
|
|
|
timestamp = +new Date(); |
|
2368
|
|
|
if (!timeoutId) { |
|
2369
|
|
|
timeoutId = setTimeout(later, wait); |
|
2370
|
|
|
} |
|
2371
|
|
|
}; |
|
2372
|
|
|
} |
|
2373
|
|
|
|
|
2374
|
|
|
;; |
|
2375
|
|
|
|
|
2376
|
|
|
var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/; |
|
2377
|
|
|
var ambigTimeOrZoneRegex = |
|
2378
|
|
|
/^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?)?$/; |
|
2379
|
|
|
|
|
2380
|
|
|
|
|
2381
|
|
|
// Creating |
|
2382
|
|
|
// ------------------------------------------------------------------------------------------------- |
|
2383
|
|
|
|
|
2384
|
|
|
// Creates a new moment, similar to the vanilla moment(...) constructor, but with |
|
2385
|
|
|
// extra features (ambiguous time, enhanced formatting). When gived an existing moment, |
|
2386
|
|
|
// it will function as a clone (and retain the zone of the moment). Anything else will |
|
2387
|
|
|
// result in a moment in the local zone. |
|
2388
|
|
|
fc.moment = function() { |
|
2389
|
|
|
return makeMoment(arguments); |
|
2390
|
|
|
}; |
|
2391
|
|
|
|
|
2392
|
|
|
// Sames as fc.moment, but forces the resulting moment to be in the UTC timezone. |
|
2393
|
|
|
fc.moment.utc = function() { |
|
2394
|
|
|
var mom = makeMoment(arguments, true); |
|
2395
|
|
|
|
|
2396
|
|
|
// Force it into UTC because makeMoment doesn't guarantee it. |
|
2397
|
|
|
if (mom.hasTime()) { // don't give ambiguously-timed moments a UTC zone |
|
2398
|
|
|
mom.utc(); |
|
2399
|
|
|
} |
|
2400
|
|
|
|
|
2401
|
|
|
return mom; |
|
2402
|
|
|
}; |
|
2403
|
|
|
|
|
2404
|
|
|
// Same as fc.moment, but when given an ISO8601 string, the timezone offset is preserved. |
|
2405
|
|
|
// ISO8601 strings with no timezone offset will become ambiguously zoned. |
|
2406
|
|
|
fc.moment.parseZone = function() { |
|
2407
|
|
|
return makeMoment(arguments, true, true); |
|
2408
|
|
|
}; |
|
2409
|
|
|
|
|
2410
|
|
|
// Builds an FCMoment from args. When given an existing moment, it clones. When given a native |
|
2411
|
|
|
// Date, or called with no arguments (the current time), the resulting moment will be local. |
|
2412
|
|
|
// Anything else needs to be "parsed" (a string or an array), and will be affected by: |
|
2413
|
|
|
// parseAsUTC - if there is no zone information, should we parse the input in UTC? |
|
2414
|
|
|
// parseZone - if there is zone information, should we force the zone of the moment? |
|
2415
|
|
|
function makeMoment(args, parseAsUTC, parseZone) { |
|
2416
|
|
|
var input = args[0]; |
|
2417
|
|
|
var isSingleString = args.length == 1 && typeof input === 'string'; |
|
2418
|
|
|
var isAmbigTime; |
|
2419
|
|
|
var isAmbigZone; |
|
2420
|
|
|
var ambigMatch; |
|
2421
|
|
|
var output; // an object with fields for the new FCMoment object |
|
2422
|
|
|
|
|
2423
|
|
|
if (moment.isMoment(input)) { |
|
2424
|
|
|
output = moment.apply(null, args); // clone it |
|
2425
|
|
|
|
|
2426
|
|
|
// the ambig properties have not been preserved in the clone, so reassign them |
|
2427
|
|
|
if (input._ambigTime) { |
|
2428
|
|
|
output._ambigTime = true; |
|
2429
|
|
|
} |
|
2430
|
|
|
if (input._ambigZone) { |
|
2431
|
|
|
output._ambigZone = true; |
|
2432
|
|
|
} |
|
2433
|
|
|
} |
|
2434
|
|
|
else if (isNativeDate(input) || input === undefined) { |
|
2435
|
|
|
output = moment.apply(null, args); // will be local |
|
2436
|
|
|
} |
|
2437
|
|
|
else { // "parsing" is required |
|
2438
|
|
|
isAmbigTime = false; |
|
2439
|
|
|
isAmbigZone = false; |
|
2440
|
|
|
|
|
2441
|
|
|
if (isSingleString) { |
|
2442
|
|
|
if (ambigDateOfMonthRegex.test(input)) { |
|
2443
|
|
|
// accept strings like '2014-05', but convert to the first of the month |
|
2444
|
|
|
input += '-01'; |
|
2445
|
|
|
args = [ input ]; // for when we pass it on to moment's constructor |
|
2446
|
|
|
isAmbigTime = true; |
|
2447
|
|
|
isAmbigZone = true; |
|
2448
|
|
|
} |
|
2449
|
|
|
else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) { |
|
2450
|
|
|
isAmbigTime = !ambigMatch[5]; // no time part? |
|
2451
|
|
|
isAmbigZone = true; |
|
2452
|
|
|
} |
|
2453
|
|
|
} |
|
2454
|
|
|
else if ($.isArray(input)) { |
|
2455
|
|
|
// arrays have no timezone information, so assume ambiguous zone |
|
2456
|
|
|
isAmbigZone = true; |
|
2457
|
|
|
} |
|
2458
|
|
|
// otherwise, probably a string with a format |
|
2459
|
|
|
|
|
2460
|
|
|
if (parseAsUTC) { |
|
2461
|
|
|
output = moment.utc.apply(moment, args); |
|
2462
|
|
|
} |
|
2463
|
|
|
else { |
|
2464
|
|
|
output = moment.apply(null, args); |
|
2465
|
|
|
} |
|
2466
|
|
|
|
|
2467
|
|
|
if (isAmbigTime) { |
|
2468
|
|
|
output._ambigTime = true; |
|
2469
|
|
|
output._ambigZone = true; // ambiguous time always means ambiguous zone |
|
2470
|
|
|
} |
|
2471
|
|
|
else if (parseZone) { // let's record the inputted zone somehow |
|
2472
|
|
|
if (isAmbigZone) { |
|
2473
|
|
|
output._ambigZone = true; |
|
2474
|
|
|
} |
|
2475
|
|
|
else if (isSingleString) { |
|
2476
|
|
|
output.zone(input); // if not a valid zone, will assign UTC |
|
2477
|
|
|
} |
|
2478
|
|
|
} |
|
2479
|
|
|
} |
|
2480
|
|
|
|
|
2481
|
|
|
return new FCMoment(output); |
|
2482
|
|
|
} |
|
2483
|
|
|
|
|
2484
|
|
|
// Our subclass of Moment. |
|
2485
|
|
|
// Accepts an object with the internal Moment properties that should be copied over to |
|
2486
|
|
|
// `this` object (most likely another Moment object). The values in this data must not |
|
2487
|
|
|
// be referenced by anything else (two moments sharing a Date object for example). |
|
2488
|
|
|
function FCMoment(internalData) { |
|
2489
|
|
|
extend(this, internalData); |
|
2490
|
|
|
} |
|
2491
|
|
|
|
|
2492
|
|
|
// Chain the prototype to Moment's |
|
2493
|
|
|
FCMoment.prototype = createObject(moment.fn); |
|
2494
|
|
|
|
|
2495
|
|
|
// We need this because Moment's implementation won't create an FCMoment, |
|
2496
|
|
|
// nor will it copy over the ambig flags. |
|
2497
|
|
|
FCMoment.prototype.clone = function() { |
|
2498
|
|
|
return makeMoment([ this ]); |
|
2499
|
|
|
}; |
|
2500
|
|
|
|
|
2501
|
|
|
|
|
2502
|
|
|
// Time-of-day |
|
2503
|
|
|
// ------------------------------------------------------------------------------------------------- |
|
2504
|
|
|
|
|
2505
|
|
|
// GETTER |
|
2506
|
|
|
// Returns a Duration with the hours/minutes/seconds/ms values of the moment. |
|
2507
|
|
|
// If the moment has an ambiguous time, a duration of 00:00 will be returned. |
|
2508
|
|
|
// |
|
2509
|
|
|
// SETTER |
|
2510
|
|
|
// You can supply a Duration, a Moment, or a Duration-like argument. |
|
2511
|
|
|
// When setting the time, and the moment has an ambiguous time, it then becomes unambiguous. |
|
2512
|
|
|
FCMoment.prototype.time = function(time) { |
|
2513
|
|
|
if (time == null) { // getter |
|
2514
|
|
|
return moment.duration({ |
|
2515
|
|
|
hours: this.hours(), |
|
2516
|
|
|
minutes: this.minutes(), |
|
2517
|
|
|
seconds: this.seconds(), |
|
2518
|
|
|
milliseconds: this.milliseconds() |
|
2519
|
|
|
}); |
|
2520
|
|
|
} |
|
2521
|
|
|
else { // setter |
|
2522
|
|
|
|
|
2523
|
|
|
delete this._ambigTime; // mark that the moment now has a time |
|
2524
|
|
|
|
|
2525
|
|
|
if (!moment.isDuration(time) && !moment.isMoment(time)) { |
|
2526
|
|
|
time = moment.duration(time); |
|
2527
|
|
|
} |
|
2528
|
|
|
|
|
2529
|
|
|
// The day value should cause overflow (so 24 hours becomes 00:00:00 of next day). |
|
2530
|
|
|
// Only for Duration times, not Moment times. |
|
2531
|
|
|
var dayHours = 0; |
|
2532
|
|
|
if (moment.isDuration(time)) { |
|
2533
|
|
|
dayHours = Math.floor(time.asDays()) * 24; |
|
2534
|
|
|
} |
|
2535
|
|
|
|
|
2536
|
|
|
// We need to set the individual fields. |
|
2537
|
|
|
// Can't use startOf('day') then add duration. In case of DST at start of day. |
|
2538
|
|
|
return this.hours(dayHours + time.hours()) |
|
2539
|
|
|
.minutes(time.minutes()) |
|
2540
|
|
|
.seconds(time.seconds()) |
|
2541
|
|
|
.milliseconds(time.milliseconds()); |
|
2542
|
|
|
} |
|
2543
|
|
|
}; |
|
2544
|
|
|
|
|
2545
|
|
|
// Converts the moment to UTC, stripping out its time-of-day and timezone offset, |
|
2546
|
|
|
// but preserving its YMD. A moment with a stripped time will display no time |
|
2547
|
|
|
// nor timezone offset when .format() is called. |
|
2548
|
|
|
FCMoment.prototype.stripTime = function() { |
|
2549
|
|
|
var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array |
|
2550
|
|
|
|
|
2551
|
|
|
// set the internal UTC flag |
|
2552
|
|
|
moment.fn.utc.call(this); // call the original method, because we don't want to affect _ambigZone |
|
2553
|
|
|
|
|
2554
|
|
|
this.year(a[0]) // TODO: find a way to do this in one shot |
|
2555
|
|
|
.month(a[1]) |
|
2556
|
|
|
.date(a[2]) |
|
2557
|
|
|
.hours(0) |
|
2558
|
|
|
.minutes(0) |
|
2559
|
|
|
.seconds(0) |
|
2560
|
|
|
.milliseconds(0); |
|
2561
|
|
|
|
|
2562
|
|
|
// Mark the time as ambiguous. This needs to happen after the .utc() call, which calls .zone(), which |
|
2563
|
|
|
// clears all ambig flags. Same concept with the .year/month/date calls in the case of moment-timezone. |
|
2564
|
|
|
this._ambigTime = true; |
|
2565
|
|
|
this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset |
|
2566
|
|
|
|
|
2567
|
|
|
return this; // for chaining |
|
2568
|
|
|
}; |
|
2569
|
|
|
|
|
2570
|
|
|
// Returns if the moment has a non-ambiguous time (boolean) |
|
2571
|
|
|
FCMoment.prototype.hasTime = function() { |
|
2572
|
|
|
return !this._ambigTime; |
|
2573
|
|
|
}; |
|
2574
|
|
|
|
|
2575
|
|
|
|
|
2576
|
|
|
// Timezone |
|
2577
|
|
|
// ------------------------------------------------------------------------------------------------- |
|
2578
|
|
|
|
|
2579
|
|
|
// Converts the moment to UTC, stripping out its timezone offset, but preserving its |
|
2580
|
|
|
// YMD and time-of-day. A moment with a stripped timezone offset will display no |
|
2581
|
|
|
// timezone offset when .format() is called. |
|
2582
|
|
|
FCMoment.prototype.stripZone = function() { |
|
2583
|
|
|
var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array |
|
2584
|
|
|
var wasAmbigTime = this._ambigTime; |
|
2585
|
|
|
|
|
2586
|
|
|
moment.fn.utc.call(this); // set the internal UTC flag |
|
2587
|
|
|
|
|
2588
|
|
|
this.year(a[0]) // TODO: find a way to do this in one shot |
|
2589
|
|
|
.month(a[1]) |
|
2590
|
|
|
.date(a[2]) |
|
2591
|
|
|
.hours(a[3]) |
|
2592
|
|
|
.minutes(a[4]) |
|
2593
|
|
|
.seconds(a[5]) |
|
2594
|
|
|
.milliseconds(a[6]); |
|
2595
|
|
|
|
|
2596
|
|
|
if (wasAmbigTime) { |
|
2597
|
|
|
// the above call to .utc()/.zone() unfortunately clears the ambig flags, so reassign |
|
2598
|
|
|
this._ambigTime = true; |
|
2599
|
|
|
} |
|
2600
|
|
|
|
|
2601
|
|
|
// Mark the zone as ambiguous. This needs to happen after the .utc() call, which calls .zone(), which |
|
2602
|
|
|
// clears all ambig flags. Same concept with the .year/month/date calls in the case of moment-timezone. |
|
2603
|
|
|
this._ambigZone = true; |
|
2604
|
|
|
|
|
2605
|
|
|
return this; // for chaining |
|
2606
|
|
|
}; |
|
2607
|
|
|
|
|
2608
|
|
|
// Returns of the moment has a non-ambiguous timezone offset (boolean) |
|
2609
|
|
|
FCMoment.prototype.hasZone = function() { |
|
2610
|
|
|
return !this._ambigZone; |
|
2611
|
|
|
}; |
|
2612
|
|
|
|
|
2613
|
|
|
// this method implicitly marks a zone |
|
2614
|
|
|
FCMoment.prototype.zone = function(tzo) { |
|
2615
|
|
|
|
|
2616
|
|
|
if (tzo != null) { |
|
2617
|
|
|
// FYI, the delete statements need to be before the .zone() call or else chaos ensues |
|
2618
|
|
|
// for reasons I don't understand. |
|
2619
|
|
|
delete this._ambigTime; |
|
2620
|
|
|
delete this._ambigZone; |
|
2621
|
|
|
} |
|
2622
|
|
|
|
|
2623
|
|
|
return moment.fn.zone.apply(this, arguments); |
|
2624
|
|
|
}; |
|
2625
|
|
|
|
|
2626
|
|
|
// this method implicitly marks a zone |
|
2627
|
|
|
FCMoment.prototype.local = function() { |
|
2628
|
|
|
var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array |
|
2629
|
|
|
var wasAmbigZone = this._ambigZone; |
|
2630
|
|
|
|
|
2631
|
|
|
// will happen anyway via .local()/.zone(), but don't want to rely on internal implementation |
|
2632
|
|
|
delete this._ambigTime; |
|
2633
|
|
|
delete this._ambigZone; |
|
2634
|
|
|
|
|
2635
|
|
|
moment.fn.local.apply(this, arguments); |
|
2636
|
|
|
|
|
2637
|
|
|
if (wasAmbigZone) { |
|
2638
|
|
|
// If the moment was ambiguously zoned, the date fields were stored as UTC. |
|
2639
|
|
|
// We want to preserve these, but in local time. |
|
2640
|
|
|
this.year(a[0]) // TODO: find a way to do this in one shot |
|
2641
|
|
|
.month(a[1]) |
|
2642
|
|
|
.date(a[2]) |
|
2643
|
|
|
.hours(a[3]) |
|
2644
|
|
|
.minutes(a[4]) |
|
2645
|
|
|
.seconds(a[5]) |
|
2646
|
|
|
.milliseconds(a[6]); |
|
2647
|
|
|
} |
|
2648
|
|
|
|
|
2649
|
|
|
return this; // for chaining |
|
2650
|
|
|
}; |
|
2651
|
|
|
|
|
2652
|
|
|
// this method implicitly marks a zone |
|
2653
|
|
|
FCMoment.prototype.utc = function() { |
|
2654
|
|
|
|
|
2655
|
|
|
// will happen anyway via .local()/.zone(), but don't want to rely on internal implementation |
|
2656
|
|
|
delete this._ambigTime; |
|
2657
|
|
|
delete this._ambigZone; |
|
2658
|
|
|
|
|
2659
|
|
|
return moment.fn.utc.apply(this, arguments); |
|
2660
|
|
|
}; |
|
2661
|
|
|
|
|
2662
|
|
|
|
|
2663
|
|
|
// Formatting |
|
2664
|
|
|
// ------------------------------------------------------------------------------------------------- |
|
2665
|
|
|
|
|
2666
|
|
|
FCMoment.prototype.format = function() { |
|
2667
|
|
|
if (arguments[0]) { |
|
2668
|
|
|
return formatDate(this, arguments[0]); // our extended formatting |
|
2669
|
|
|
} |
|
2670
|
|
|
if (this._ambigTime) { |
|
2671
|
|
|
return momentFormat(this, 'YYYY-MM-DD'); |
|
2672
|
|
|
} |
|
2673
|
|
|
if (this._ambigZone) { |
|
2674
|
|
|
return momentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss'); |
|
2675
|
|
|
} |
|
2676
|
|
|
return momentFormat(this); // default moment original formatting |
|
2677
|
|
|
}; |
|
2678
|
|
|
|
|
2679
|
|
|
FCMoment.prototype.toISOString = function() { |
|
2680
|
|
|
if (this._ambigTime) { |
|
2681
|
|
|
return momentFormat(this, 'YYYY-MM-DD'); |
|
2682
|
|
|
} |
|
2683
|
|
|
if (this._ambigZone) { |
|
2684
|
|
|
return momentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss'); |
|
2685
|
|
|
} |
|
2686
|
|
|
return moment.fn.toISOString.apply(this, arguments); |
|
2687
|
|
|
}; |
|
2688
|
|
|
|
|
2689
|
|
|
|
|
2690
|
|
|
// Querying |
|
2691
|
|
|
// ------------------------------------------------------------------------------------------------- |
|
2692
|
|
|
|
|
2693
|
|
|
// Is the moment within the specified range? `end` is exclusive. |
|
2694
|
|
|
FCMoment.prototype.isWithin = function(start, end) { |
|
2695
|
|
|
var a = commonlyAmbiguate([ this, start, end ]); |
|
2696
|
|
|
return a[0] >= a[1] && a[0] < a[2]; |
|
2697
|
|
|
}; |
|
2698
|
|
|
|
|
2699
|
|
|
// When isSame is called with units, timezone ambiguity is normalized before the comparison happens. |
|
2700
|
|
|
// If no units are specified, the two moments must be identically the same, with matching ambig flags. |
|
2701
|
|
|
FCMoment.prototype.isSame = function(input, units) { |
|
2702
|
|
|
var a; |
|
2703
|
|
|
|
|
2704
|
|
|
if (units) { |
|
2705
|
|
|
a = commonlyAmbiguate([ this, input ], true); // normalize timezones but don't erase times |
|
2706
|
|
|
return moment.fn.isSame.call(a[0], a[1], units); |
|
2707
|
|
|
} |
|
2708
|
|
|
else { |
|
2709
|
|
|
input = fc.moment.parseZone(input); // normalize input |
|
2710
|
|
|
return moment.fn.isSame.call(this, input) && |
|
2711
|
|
|
Boolean(this._ambigTime) === Boolean(input._ambigTime) && |
|
2712
|
|
|
Boolean(this._ambigZone) === Boolean(input._ambigZone); |
|
2713
|
|
|
} |
|
2714
|
|
|
}; |
|
2715
|
|
|
|
|
2716
|
|
|
// Make these query methods work with ambiguous moments |
|
2717
|
|
|
$.each([ |
|
2718
|
|
|
'isBefore', |
|
2719
|
|
|
'isAfter' |
|
2720
|
|
|
], function(i, methodName) { |
|
2721
|
|
|
FCMoment.prototype[methodName] = function(input, units) { |
|
2722
|
|
|
var a = commonlyAmbiguate([ this, input ]); |
|
2723
|
|
|
return moment.fn[methodName].call(a[0], a[1], units); |
|
2724
|
|
|
}; |
|
2725
|
|
|
}); |
|
2726
|
|
|
|
|
2727
|
|
|
|
|
2728
|
|
|
// Misc Internals |
|
2729
|
|
|
// ------------------------------------------------------------------------------------------------- |
|
2730
|
|
|
|
|
2731
|
|
|
// given an array of moment-like inputs, return a parallel array w/ moments similarly ambiguated. |
|
2732
|
|
|
// for example, of one moment has ambig time, but not others, all moments will have their time stripped. |
|
2733
|
|
|
// set `preserveTime` to `true` to keep times, but only normalize zone ambiguity. |
|
2734
|
|
|
function commonlyAmbiguate(inputs, preserveTime) { |
|
2735
|
|
|
var outputs = []; |
|
2736
|
|
|
var anyAmbigTime = false; |
|
2737
|
|
|
var anyAmbigZone = false; |
|
2738
|
|
|
var i; |
|
2739
|
|
|
|
|
2740
|
|
|
for (i=0; i<inputs.length; i++) { |
|
2741
|
|
|
outputs.push(fc.moment.parseZone(inputs[i])); |
|
2742
|
|
|
anyAmbigTime = anyAmbigTime || outputs[i]._ambigTime; |
|
2743
|
|
|
anyAmbigZone = anyAmbigZone || outputs[i]._ambigZone; |
|
2744
|
|
|
} |
|
2745
|
|
|
|
|
2746
|
|
|
for (i=0; i<outputs.length; i++) { |
|
2747
|
|
|
if (anyAmbigTime && !preserveTime) { |
|
2748
|
|
|
outputs[i].stripTime(); |
|
2749
|
|
|
} |
|
2750
|
|
|
else if (anyAmbigZone) { |
|
2751
|
|
|
outputs[i].stripZone(); |
|
2752
|
|
|
} |
|
2753
|
|
|
} |
|
2754
|
|
|
|
|
2755
|
|
|
return outputs; |
|
2756
|
|
|
} |
|
2757
|
|
|
|
|
2758
|
|
|
;; |
|
2759
|
|
|
|
|
2760
|
|
|
// Single Date Formatting |
|
2761
|
|
|
// ------------------------------------------------------------------------------------------------- |
|
2762
|
|
|
|
|
2763
|
|
|
|
|
2764
|
|
|
// call this if you want Moment's original format method to be used |
|
2765
|
|
|
function momentFormat(mom, formatStr) { |
|
2766
|
|
|
return moment.fn.format.call(mom, formatStr); |
|
2767
|
|
|
} |
|
2768
|
|
|
|
|
2769
|
|
|
|
|
2770
|
|
|
// Formats `date` with a Moment formatting string, but allow our non-zero areas and |
|
2771
|
|
|
// additional token. |
|
2772
|
|
|
function formatDate(date, formatStr) { |
|
2773
|
|
|
return formatDateWithChunks(date, getFormatStringChunks(formatStr)); |
|
2774
|
|
|
} |
|
2775
|
|
|
|
|
2776
|
|
|
|
|
2777
|
|
|
function formatDateWithChunks(date, chunks) { |
|
2778
|
|
|
var s = ''; |
|
2779
|
|
|
var i; |
|
2780
|
|
|
|
|
2781
|
|
|
for (i=0; i<chunks.length; i++) { |
|
2782
|
|
|
s += formatDateWithChunk(date, chunks[i]); |
|
2783
|
|
|
} |
|
2784
|
|
|
|
|
2785
|
|
|
return s; |
|
2786
|
|
|
} |
|
2787
|
|
|
|
|
2788
|
|
|
|
|
2789
|
|
|
// addition formatting tokens we want recognized |
|
2790
|
|
|
var tokenOverrides = { |
|
2791
|
|
|
t: function(date) { // "a" or "p" |
|
2792
|
|
|
return momentFormat(date, 'a').charAt(0); |
|
2793
|
|
|
}, |
|
2794
|
|
|
T: function(date) { // "A" or "P" |
|
2795
|
|
|
return momentFormat(date, 'A').charAt(0); |
|
2796
|
|
|
} |
|
2797
|
|
|
}; |
|
2798
|
|
|
|
|
2799
|
|
|
|
|
2800
|
|
|
function formatDateWithChunk(date, chunk) { |
|
2801
|
|
|
var token; |
|
2802
|
|
|
var maybeStr; |
|
2803
|
|
|
|
|
2804
|
|
|
if (typeof chunk === 'string') { // a literal string |
|
2805
|
|
|
return chunk; |
|
2806
|
|
|
} |
|
2807
|
|
|
else if ((token = chunk.token)) { // a token, like "YYYY" |
|
2808
|
|
|
if (tokenOverrides[token]) { |
|
2809
|
|
|
return tokenOverrides[token](date); // use our custom token |
|
2810
|
|
|
} |
|
2811
|
|
|
return momentFormat(date, token); |
|
2812
|
|
|
} |
|
2813
|
|
|
else if (chunk.maybe) { // a grouping of other chunks that must be non-zero |
|
2814
|
|
|
maybeStr = formatDateWithChunks(date, chunk.maybe); |
|
2815
|
|
|
if (maybeStr.match(/[1-9]/)) { |
|
2816
|
|
|
return maybeStr; |
|
2817
|
|
|
} |
|
2818
|
|
|
} |
|
2819
|
|
|
|
|
2820
|
|
|
return ''; |
|
2821
|
|
|
} |
|
2822
|
|
|
|
|
2823
|
|
|
|
|
2824
|
|
|
// Date Range Formatting |
|
2825
|
|
|
// ------------------------------------------------------------------------------------------------- |
|
2826
|
|
|
// TODO: make it work with timezone offset |
|
2827
|
|
|
|
|
2828
|
|
|
// Using a formatting string meant for a single date, generate a range string, like |
|
2829
|
|
|
// "Sep 2 - 9 2013", that intelligently inserts a separator where the dates differ. |
|
2830
|
|
|
// If the dates are the same as far as the format string is concerned, just return a single |
|
2831
|
|
|
// rendering of one date, without any separator. |
|
2832
|
|
|
function formatRange(date1, date2, formatStr, separator, isRTL) { |
|
2833
|
|
|
var localeData; |
|
2834
|
|
|
|
|
2835
|
|
|
date1 = fc.moment.parseZone(date1); |
|
2836
|
|
|
date2 = fc.moment.parseZone(date2); |
|
2837
|
|
|
|
|
2838
|
|
|
localeData = (date1.localeData || date1.lang).call(date1); // works with moment-pre-2.8 |
|
2839
|
|
|
|
|
2840
|
|
|
// Expand localized format strings, like "LL" -> "MMMM D YYYY" |
|
2841
|
|
|
formatStr = localeData.longDateFormat(formatStr) || formatStr; |
|
2842
|
|
|
// BTW, this is not important for `formatDate` because it is impossible to put custom tokens |
|
2843
|
|
|
// or non-zero areas in Moment's localized format strings. |
|
2844
|
|
|
|
|
2845
|
|
|
separator = separator || ' - '; |
|
2846
|
|
|
|
|
2847
|
|
|
return formatRangeWithChunks( |
|
2848
|
|
|
date1, |
|
2849
|
|
|
date2, |
|
2850
|
|
|
getFormatStringChunks(formatStr), |
|
2851
|
|
|
separator, |
|
2852
|
|
|
isRTL |
|
2853
|
|
|
); |
|
2854
|
|
|
} |
|
2855
|
|
|
fc.formatRange = formatRange; // expose |
|
2856
|
|
|
|
|
2857
|
|
|
|
|
2858
|
|
|
function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) { |
|
2859
|
|
|
var chunkStr; // the rendering of the chunk |
|
2860
|
|
|
var leftI; |
|
2861
|
|
|
var leftStr = ''; |
|
2862
|
|
|
var rightI; |
|
2863
|
|
|
var rightStr = ''; |
|
2864
|
|
|
var middleI; |
|
2865
|
|
|
var middleStr1 = ''; |
|
2866
|
|
|
var middleStr2 = ''; |
|
2867
|
|
|
var middleStr = ''; |
|
2868
|
|
|
|
|
2869
|
|
|
// Start at the leftmost side of the formatting string and continue until you hit a token |
|
2870
|
|
|
// that is not the same between dates. |
|
2871
|
|
|
for (leftI=0; leftI<chunks.length; leftI++) { |
|
2872
|
|
|
chunkStr = formatSimilarChunk(date1, date2, chunks[leftI]); |
|
2873
|
|
|
if (chunkStr === false) { |
|
2874
|
|
|
break; |
|
2875
|
|
|
} |
|
2876
|
|
|
leftStr += chunkStr; |
|
2877
|
|
|
} |
|
2878
|
|
|
|
|
2879
|
|
|
// Similarly, start at the rightmost side of the formatting string and move left |
|
2880
|
|
|
for (rightI=chunks.length-1; rightI>leftI; rightI--) { |
|
2881
|
|
|
chunkStr = formatSimilarChunk(date1, date2, chunks[rightI]); |
|
2882
|
|
|
if (chunkStr === false) { |
|
2883
|
|
|
break; |
|
2884
|
|
|
} |
|
2885
|
|
|
rightStr = chunkStr + rightStr; |
|
2886
|
|
|
} |
|
2887
|
|
|
|
|
2888
|
|
|
// The area in the middle is different for both of the dates. |
|
2889
|
|
|
// Collect them distinctly so we can jam them together later. |
|
2890
|
|
|
for (middleI=leftI; middleI<=rightI; middleI++) { |
|
2891
|
|
|
middleStr1 += formatDateWithChunk(date1, chunks[middleI]); |
|
2892
|
|
|
middleStr2 += formatDateWithChunk(date2, chunks[middleI]); |
|
2893
|
|
|
} |
|
2894
|
|
|
|
|
2895
|
|
|
if (middleStr1 || middleStr2) { |
|
2896
|
|
|
if (isRTL) { |
|
2897
|
|
|
middleStr = middleStr2 + separator + middleStr1; |
|
2898
|
|
|
} |
|
2899
|
|
|
else { |
|
2900
|
|
|
middleStr = middleStr1 + separator + middleStr2; |
|
2901
|
|
|
} |
|
2902
|
|
|
} |
|
2903
|
|
|
|
|
2904
|
|
|
return leftStr + middleStr + rightStr; |
|
2905
|
|
|
} |
|
2906
|
|
|
|
|
2907
|
|
|
|
|
2908
|
|
|
var similarUnitMap = { |
|
2909
|
|
|
Y: 'year', |
|
2910
|
|
|
M: 'month', |
|
2911
|
|
|
D: 'day', // day of month |
|
2912
|
|
|
d: 'day', // day of week |
|
2913
|
|
|
// prevents a separator between anything time-related... |
|
2914
|
|
|
A: 'second', // AM/PM |
|
2915
|
|
|
a: 'second', // am/pm |
|
2916
|
|
|
T: 'second', // A/P |
|
2917
|
|
|
t: 'second', // a/p |
|
2918
|
|
|
H: 'second', // hour (24) |
|
2919
|
|
|
h: 'second', // hour (12) |
|
2920
|
|
|
m: 'second', // minute |
|
2921
|
|
|
s: 'second' // second |
|
2922
|
|
|
}; |
|
2923
|
|
|
// TODO: week maybe? |
|
2924
|
|
|
|
|
2925
|
|
|
|
|
2926
|
|
|
// Given a formatting chunk, and given that both dates are similar in the regard the |
|
2927
|
|
|
// formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`. |
|
2928
|
|
|
function formatSimilarChunk(date1, date2, chunk) { |
|
2929
|
|
|
var token; |
|
2930
|
|
|
var unit; |
|
2931
|
|
|
|
|
2932
|
|
|
if (typeof chunk === 'string') { // a literal string |
|
2933
|
|
|
return chunk; |
|
2934
|
|
|
} |
|
2935
|
|
|
else if ((token = chunk.token)) { |
|
2936
|
|
|
unit = similarUnitMap[token.charAt(0)]; |
|
2937
|
|
|
// are the dates the same for this unit of measurement? |
|
2938
|
|
|
if (unit && date1.isSame(date2, unit)) { |
|
2939
|
|
|
return momentFormat(date1, token); // would be the same if we used `date2` |
|
2940
|
|
|
// BTW, don't support custom tokens |
|
2941
|
|
|
} |
|
2942
|
|
|
} |
|
2943
|
|
|
|
|
2944
|
|
|
return false; // the chunk is NOT the same for the two dates |
|
2945
|
|
|
// BTW, don't support splitting on non-zero areas |
|
2946
|
|
|
} |
|
2947
|
|
|
|
|
2948
|
|
|
|
|
2949
|
|
|
// Chunking Utils |
|
2950
|
|
|
// ------------------------------------------------------------------------------------------------- |
|
2951
|
|
|
|
|
2952
|
|
|
|
|
2953
|
|
|
var formatStringChunkCache = {}; |
|
2954
|
|
|
|
|
2955
|
|
|
|
|
2956
|
|
|
function getFormatStringChunks(formatStr) { |
|
2957
|
|
|
if (formatStr in formatStringChunkCache) { |
|
2958
|
|
|
return formatStringChunkCache[formatStr]; |
|
2959
|
|
|
} |
|
2960
|
|
|
return (formatStringChunkCache[formatStr] = chunkFormatString(formatStr)); |
|
2961
|
|
|
} |
|
2962
|
|
|
|
|
2963
|
|
|
|
|
2964
|
|
|
// Break the formatting string into an array of chunks |
|
2965
|
|
|
function chunkFormatString(formatStr) { |
|
2966
|
|
|
var chunks = []; |
|
2967
|
|
|
var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LT|(\w)\4*o?)|([^\w\[\(]+)/g; // TODO: more descrimination |
|
2968
|
|
|
var match; |
|
2969
|
|
|
|
|
2970
|
|
|
while ((match = chunker.exec(formatStr))) { |
|
2971
|
|
|
if (match[1]) { // a literal string inside [ ... ] |
|
2972
|
|
|
chunks.push(match[1]); |
|
2973
|
|
|
} |
|
2974
|
|
|
else if (match[2]) { // non-zero formatting inside ( ... ) |
|
2975
|
|
|
chunks.push({ maybe: chunkFormatString(match[2]) }); |
|
2976
|
|
|
} |
|
2977
|
|
|
else if (match[3]) { // a formatting token |
|
2978
|
|
|
chunks.push({ token: match[3] }); |
|
2979
|
|
|
} |
|
2980
|
|
|
else if (match[5]) { // an unenclosed literal string |
|
2981
|
|
|
chunks.push(match[5]); |
|
2982
|
|
|
} |
|
2983
|
|
|
} |
|
2984
|
|
|
|
|
2985
|
|
|
return chunks; |
|
2986
|
|
|
} |
|
2987
|
|
|
|
|
2988
|
|
|
;; |
|
2989
|
|
|
|
|
2990
|
|
|
/* A rectangular panel that is absolutely positioned over other content |
|
2991
|
|
|
------------------------------------------------------------------------------------------------------------------------ |
|
2992
|
|
|
Options: |
|
2993
|
|
|
- className (string) |
|
2994
|
|
|
- content (HTML string or jQuery element set) |
|
2995
|
|
|
- parentEl |
|
2996
|
|
|
- top |
|
2997
|
|
|
- left |
|
2998
|
|
|
- right (the x coord of where the right edge should be. not a "CSS" right) |
|
2999
|
|
|
- autoHide (boolean) |
|
3000
|
|
|
- show (callback) |
|
3001
|
|
|
- hide (callback) |
|
3002
|
|
|
*/ |
|
3003
|
|
|
|
|
3004
|
|
|
function Popover(options) { |
|
3005
|
|
|
this.options = options || {}; |
|
3006
|
|
|
} |
|
3007
|
|
|
|
|
3008
|
|
|
|
|
3009
|
|
|
Popover.prototype = { |
|
3010
|
|
|
|
|
3011
|
|
|
isHidden: true, |
|
3012
|
|
|
options: null, |
|
3013
|
|
|
el: null, // the container element for the popover. generated by this object |
|
3014
|
|
|
documentMousedownProxy: null, // document mousedown handler bound to `this` |
|
3015
|
|
|
margin: 10, // the space required between the popover and the edges of the scroll container |
|
3016
|
|
|
|
|
3017
|
|
|
|
|
3018
|
|
|
// Shows the popover on the specified position. Renders it if not already |
|
3019
|
|
|
show: function() { |
|
3020
|
|
|
if (this.isHidden) { |
|
3021
|
|
|
if (!this.el) { |
|
3022
|
|
|
this.render(); |
|
3023
|
|
|
} |
|
3024
|
|
|
this.el.show(); |
|
3025
|
|
|
this.position(); |
|
3026
|
|
|
this.isHidden = false; |
|
3027
|
|
|
this.trigger('show'); |
|
3028
|
|
|
} |
|
3029
|
|
|
}, |
|
3030
|
|
|
|
|
3031
|
|
|
|
|
3032
|
|
|
// Hides the popover, through CSS, but does not remove it from the DOM |
|
3033
|
|
|
hide: function() { |
|
3034
|
|
|
if (!this.isHidden) { |
|
3035
|
|
|
this.el.hide(); |
|
3036
|
|
|
this.isHidden = true; |
|
3037
|
|
|
this.trigger('hide'); |
|
3038
|
|
|
} |
|
3039
|
|
|
}, |
|
3040
|
|
|
|
|
3041
|
|
|
|
|
3042
|
|
|
// Creates `this.el` and renders content inside of it |
|
3043
|
|
|
render: function() { |
|
3044
|
|
|
var _this = this; |
|
3045
|
|
|
var options = this.options; |
|
3046
|
|
|
|
|
3047
|
|
|
this.el = $('<div class="fc-popover"/>') |
|
3048
|
|
|
.addClass(options.className || '') |
|
3049
|
|
|
.css({ |
|
3050
|
|
|
// position initially to the top left to avoid creating scrollbars |
|
3051
|
|
|
top: 0, |
|
3052
|
|
|
left: 0 |
|
3053
|
|
|
}) |
|
3054
|
|
|
.append(options.content) |
|
3055
|
|
|
.appendTo(options.parentEl); |
|
3056
|
|
|
|
|
3057
|
|
|
// when a click happens on anything inside with a 'fc-close' className, hide the popover |
|
3058
|
|
|
this.el.on('click', '.fc-close', function() { |
|
3059
|
|
|
_this.hide(); |
|
3060
|
|
|
}); |
|
3061
|
|
|
|
|
3062
|
|
|
if (options.autoHide) { |
|
3063
|
|
|
$(document).on('mousedown', this.documentMousedownProxy = $.proxy(this, 'documentMousedown')); |
|
3064
|
|
|
} |
|
3065
|
|
|
}, |
|
3066
|
|
|
|
|
3067
|
|
|
|
|
3068
|
|
|
// Triggered when the user clicks *anywhere* in the document, for the autoHide feature |
|
3069
|
|
|
documentMousedown: function(ev) { |
|
3070
|
|
|
// only hide the popover if the click happened outside the popover |
|
3071
|
|
|
if (this.el && !$(ev.target).closest(this.el).length) { |
|
3072
|
|
|
this.hide(); |
|
3073
|
|
|
} |
|
3074
|
|
|
}, |
|
3075
|
|
|
|
|
3076
|
|
|
|
|
3077
|
|
|
// Hides and unregisters any handlers |
|
3078
|
|
|
destroy: function() { |
|
3079
|
|
|
this.hide(); |
|
3080
|
|
|
|
|
3081
|
|
|
if (this.el) { |
|
3082
|
|
|
this.el.remove(); |
|
3083
|
|
|
this.el = null; |
|
3084
|
|
|
} |
|
3085
|
|
|
|
|
3086
|
|
|
$(document).off('mousedown', this.documentMousedownProxy); |
|
3087
|
|
|
}, |
|
3088
|
|
|
|
|
3089
|
|
|
|
|
3090
|
|
|
// Positions the popover optimally, using the top/left/right options |
|
3091
|
|
|
position: function() { |
|
3092
|
|
|
var options = this.options; |
|
3093
|
|
|
var origin = this.el.offsetParent().offset(); |
|
3094
|
|
|
var width = this.el.outerWidth(); |
|
3095
|
|
|
var height = this.el.outerHeight(); |
|
3096
|
|
|
var windowEl = $(window); |
|
3097
|
|
|
var viewportEl = getScrollParent(this.el); |
|
3098
|
|
|
var viewportTop; |
|
3099
|
|
|
var viewportLeft; |
|
3100
|
|
|
var viewportOffset; |
|
3101
|
|
|
var top; // the "position" (not "offset") values for the popover |
|
3102
|
|
|
var left; // |
|
3103
|
|
|
|
|
3104
|
|
|
// compute top and left |
|
3105
|
|
|
top = options.top || 0; |
|
3106
|
|
|
if (options.left !== undefined) { |
|
3107
|
|
|
left = options.left; |
|
3108
|
|
|
} |
|
3109
|
|
|
else if (options.right !== undefined) { |
|
3110
|
|
|
left = options.right - width; // derive the left value from the right value |
|
3111
|
|
|
} |
|
3112
|
|
|
else { |
|
3113
|
|
|
left = 0; |
|
3114
|
|
|
} |
|
3115
|
|
|
|
|
3116
|
|
|
if (viewportEl.is(window) || viewportEl.is(document)) { // normalize getScrollParent's result |
|
3117
|
|
|
viewportEl = windowEl; |
|
3118
|
|
|
viewportTop = 0; // the window is always at the top left |
|
3119
|
|
|
viewportLeft = 0; // (and .offset() won't work if called here) |
|
3120
|
|
|
} |
|
3121
|
|
|
else { |
|
3122
|
|
|
viewportOffset = viewportEl.offset(); |
|
3123
|
|
|
viewportTop = viewportOffset.top; |
|
3124
|
|
|
viewportLeft = viewportOffset.left; |
|
3125
|
|
|
} |
|
3126
|
|
|
|
|
3127
|
|
|
// if the window is scrolled, it causes the visible area to be further down |
|
3128
|
|
|
viewportTop += windowEl.scrollTop(); |
|
3129
|
|
|
viewportLeft += windowEl.scrollLeft(); |
|
3130
|
|
|
|
|
3131
|
|
|
// constrain to the view port. if constrained by two edges, give precedence to top/left |
|
3132
|
|
|
if (options.viewportConstrain !== false) { |
|
3133
|
|
|
top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin); |
|
3134
|
|
|
top = Math.max(top, viewportTop + this.margin); |
|
3135
|
|
|
left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin); |
|
3136
|
|
|
left = Math.max(left, viewportLeft + this.margin); |
|
3137
|
|
|
} |
|
3138
|
|
|
|
|
3139
|
|
|
this.el.css({ |
|
3140
|
|
|
top: top - origin.top, |
|
3141
|
|
|
left: left - origin.left |
|
3142
|
|
|
}); |
|
3143
|
|
|
}, |
|
3144
|
|
|
|
|
3145
|
|
|
|
|
3146
|
|
|
// Triggers a callback. Calls a function in the option hash of the same name. |
|
3147
|
|
|
// Arguments beyond the first `name` are forwarded on. |
|
3148
|
|
|
// TODO: better code reuse for this. Repeat code |
|
3149
|
|
|
trigger: function(name) { |
|
3150
|
|
|
if (this.options[name]) { |
|
3151
|
|
|
this.options[name].apply(this, Array.prototype.slice.call(arguments, 1)); |
|
3152
|
|
|
} |
|
3153
|
|
|
} |
|
3154
|
|
|
|
|
3155
|
|
|
}; |
|
3156
|
|
|
|
|
3157
|
|
|
;; |
|
3158
|
|
|
|
|
3159
|
|
|
/* A "coordinate map" converts pixel coordinates into an associated cell, which has an associated date |
|
3160
|
|
|
------------------------------------------------------------------------------------------------------------------------ |
|
3161
|
|
|
Common interface: |
|
3162
|
|
|
|
|
3163
|
|
|
CoordMap.prototype = { |
|
3164
|
|
|
build: function() {}, |
|
3165
|
|
|
getCell: function(x, y) {} |
|
3166
|
|
|
}; |
|
3167
|
|
|
|
|
3168
|
|
|
*/ |
|
3169
|
|
|
|
|
3170
|
|
|
/* Coordinate map for a grid component |
|
3171
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
|
3172
|
|
|
|
|
3173
|
|
|
function GridCoordMap(grid) { |
|
3174
|
|
|
this.grid = grid; |
|
3175
|
|
|
} |
|
3176
|
|
|
|
|
3177
|
|
|
|
|
3178
|
|
|
GridCoordMap.prototype = { |
|
3179
|
|
|
|
|
3180
|
|
|
grid: null, // reference to the Grid |
|
3181
|
|
|
rows: null, // the top-to-bottom y coordinates. including the bottom of the last item |
|
3182
|
|
|
cols: null, // the left-to-right x coordinates. including the right of the last item |
|
3183
|
|
|
|
|
3184
|
|
|
containerEl: null, // container element that all coordinates are constrained to. optionally assigned |
|
3185
|
|
|
minX: null, |
|
3186
|
|
|
maxX: null, // exclusive |
|
3187
|
|
|
minY: null, |
|
3188
|
|
|
maxY: null, // exclusive |
|
3189
|
|
|
|
|
3190
|
|
|
|
|
3191
|
|
|
// Queries the grid for the coordinates of all the cells |
|
3192
|
|
|
build: function() { |
|
3193
|
|
|
this.grid.buildCoords( |
|
3194
|
|
|
this.rows = [], |
|
3195
|
|
|
this.cols = [] |
|
3196
|
|
|
); |
|
3197
|
|
|
this.computeBounds(); |
|
3198
|
|
|
}, |
|
3199
|
|
|
|
|
3200
|
|
|
|
|
3201
|
|
|
// Given a coordinate of the document, gets the associated cell. If no cell is underneath, returns null |
|
3202
|
|
|
getCell: function(x, y) { |
|
3203
|
|
|
var cell = null; |
|
3204
|
|
|
var rows = this.rows; |
|
3205
|
|
|
var cols = this.cols; |
|
3206
|
|
|
var r = -1; |
|
3207
|
|
|
var c = -1; |
|
3208
|
|
|
var i; |
|
3209
|
|
|
|
|
3210
|
|
|
if (this.inBounds(x, y)) { |
|
3211
|
|
|
|
|
3212
|
|
|
for (i = 0; i < rows.length; i++) { |
|
3213
|
|
|
if (y >= rows[i][0] && y < rows[i][1]) { |
|
3214
|
|
|
r = i; |
|
3215
|
|
|
break; |
|
3216
|
|
|
} |
|
3217
|
|
|
} |
|
3218
|
|
|
|
|
3219
|
|
|
for (i = 0; i < cols.length; i++) { |
|
3220
|
|
|
if (x >= cols[i][0] && x < cols[i][1]) { |
|
3221
|
|
|
c = i; |
|
3222
|
|
|
break; |
|
3223
|
|
|
} |
|
3224
|
|
|
} |
|
3225
|
|
|
|
|
3226
|
|
|
if (r >= 0 && c >= 0) { |
|
3227
|
|
|
cell = { row: r, col: c }; |
|
3228
|
|
|
cell.grid = this.grid; |
|
3229
|
|
|
cell.date = this.grid.getCellDate(cell); |
|
3230
|
|
|
} |
|
3231
|
|
|
} |
|
3232
|
|
|
|
|
3233
|
|
|
return cell; |
|
3234
|
|
|
}, |
|
3235
|
|
|
|
|
3236
|
|
|
|
|
3237
|
|
|
// If there is a containerEl, compute the bounds into min/max values |
|
3238
|
|
|
computeBounds: function() { |
|
3239
|
|
|
var containerOffset; |
|
3240
|
|
|
|
|
3241
|
|
|
if (this.containerEl) { |
|
3242
|
|
|
containerOffset = this.containerEl.offset(); |
|
3243
|
|
|
this.minX = containerOffset.left; |
|
3244
|
|
|
this.maxX = containerOffset.left + this.containerEl.outerWidth(); |
|
3245
|
|
|
this.minY = containerOffset.top; |
|
3246
|
|
|
this.maxY = containerOffset.top + this.containerEl.outerHeight(); |
|
3247
|
|
|
} |
|
3248
|
|
|
}, |
|
3249
|
|
|
|
|
3250
|
|
|
|
|
3251
|
|
|
// Determines if the given coordinates are in bounds. If no `containerEl`, always true |
|
3252
|
|
|
inBounds: function(x, y) { |
|
3253
|
|
|
if (this.containerEl) { |
|
3254
|
|
|
return x >= this.minX && x < this.maxX && y >= this.minY && y < this.maxY; |
|
3255
|
|
|
} |
|
3256
|
|
|
return true; |
|
3257
|
|
|
} |
|
3258
|
|
|
|
|
3259
|
|
|
}; |
|
3260
|
|
|
|
|
3261
|
|
|
|
|
3262
|
|
|
/* Coordinate map that is a combination of multiple other coordinate maps |
|
3263
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
|
3264
|
|
|
|
|
3265
|
|
|
function ComboCoordMap(coordMaps) { |
|
3266
|
|
|
this.coordMaps = coordMaps; |
|
3267
|
|
|
} |
|
3268
|
|
|
|
|
3269
|
|
|
|
|
3270
|
|
|
ComboCoordMap.prototype = { |
|
3271
|
|
|
|
|
3272
|
|
|
coordMaps: null, // an array of CoordMaps |
|
3273
|
|
|
|
|
3274
|
|
|
|
|
3275
|
|
|
// Builds all coordMaps |
|
3276
|
|
|
build: function() { |
|
3277
|
|
|
var coordMaps = this.coordMaps; |
|
3278
|
|
|
var i; |
|
3279
|
|
|
|
|
3280
|
|
|
for (i = 0; i < coordMaps.length; i++) { |
|
3281
|
|
|
coordMaps[i].build(); |
|
3282
|
|
|
} |
|
3283
|
|
|
}, |
|
3284
|
|
|
|
|
3285
|
|
|
|
|
3286
|
|
|
// Queries all coordMaps for the cell underneath the given coordinates, returning the first result |
|
3287
|
|
|
getCell: function(x, y) { |
|
3288
|
|
|
var coordMaps = this.coordMaps; |
|
3289
|
|
|
var cell = null; |
|
3290
|
|
|
var i; |
|
3291
|
|
|
|
|
3292
|
|
|
for (i = 0; i < coordMaps.length && !cell; i++) { |
|
3293
|
|
|
cell = coordMaps[i].getCell(x, y); |
|
3294
|
|
|
} |
|
3295
|
|
|
|
|
3296
|
|
|
return cell; |
|
3297
|
|
|
} |
|
3298
|
|
|
|
|
3299
|
|
|
}; |
|
3300
|
|
|
|
|
3301
|
|
|
;; |
|
3302
|
|
|
|
|
3303
|
|
|
/* Tracks mouse movements over a CoordMap and raises events about which cell the mouse is over. |
|
3304
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
|
3305
|
|
|
// TODO: implement scrolling |
|
3306
|
|
|
|
|
3307
|
|
|
function DragListener(coordMap, options) { |
|
3308
|
|
|
this.coordMap = coordMap; |
|
3309
|
|
|
this.options = options || {}; |
|
3310
|
|
|
} |
|
3311
|
|
|
|
|
3312
|
|
|
|
|
3313
|
|
|
DragListener.prototype = { |
|
3314
|
|
|
|
|
3315
|
|
|
coordMap: null, |
|
3316
|
|
|
options: null, |
|
3317
|
|
|
|
|
3318
|
|
|
isListening: false, |
|
3319
|
|
|
isDragging: false, |
|
3320
|
|
|
|
|
3321
|
|
|
// the cell/date the mouse was over when listening started |
|
3322
|
|
|
origCell: null, |
|
3323
|
|
|
origDate: null, |
|
3324
|
|
|
|
|
3325
|
|
|
// the cell/date the mouse is over |
|
3326
|
|
|
cell: null, |
|
3327
|
|
|
date: null, |
|
3328
|
|
|
|
|
3329
|
|
|
// coordinates of the initial mousedown |
|
3330
|
|
|
mouseX0: null, |
|
3331
|
|
|
mouseY0: null, |
|
3332
|
|
|
|
|
3333
|
|
|
// handler attached to the document, bound to the DragListener's `this` |
|
3334
|
|
|
mousemoveProxy: null, |
|
3335
|
|
|
mouseupProxy: null, |
|
3336
|
|
|
|
|
3337
|
|
|
scrollEl: null, |
|
3338
|
|
|
scrollBounds: null, // { top, bottom, left, right } |
|
3339
|
|
|
scrollTopVel: null, // pixels per second |
|
3340
|
|
|
scrollLeftVel: null, // pixels per second |
|
3341
|
|
|
scrollIntervalId: null, // ID of setTimeout for scrolling animation loop |
|
3342
|
|
|
scrollHandlerProxy: null, // this-scoped function for handling when scrollEl is scrolled |
|
3343
|
|
|
|
|
3344
|
|
|
scrollSensitivity: 30, // pixels from edge for scrolling to start |
|
3345
|
|
|
scrollSpeed: 200, // pixels per second, at maximum speed |
|
3346
|
|
|
scrollIntervalMs: 50, // millisecond wait between scroll increment |
|
3347
|
|
|
|
|
3348
|
|
|
|
|
3349
|
|
|
// Call this when the user does a mousedown. Will probably lead to startListening |
|
3350
|
|
|
mousedown: function(ev) { |
|
3351
|
|
|
if (isPrimaryMouseButton(ev)) { |
|
3352
|
|
|
|
|
3353
|
|
|
ev.preventDefault(); // prevents native selection in most browsers |
|
3354
|
|
|
|
|
3355
|
|
|
this.startListening(ev); |
|
3356
|
|
|
|
|
3357
|
|
|
// start the drag immediately if there is no minimum distance for a drag start |
|
3358
|
|
|
if (!this.options.distance) { |
|
3359
|
|
|
this.startDrag(ev); |
|
3360
|
|
|
} |
|
3361
|
|
|
} |
|
3362
|
|
|
}, |
|
3363
|
|
|
|
|
3364
|
|
|
|
|
3365
|
|
|
// Call this to start tracking mouse movements |
|
3366
|
|
|
startListening: function(ev) { |
|
3367
|
|
|
var scrollParent; |
|
3368
|
|
|
var cell; |
|
3369
|
|
|
|
|
3370
|
|
|
if (!this.isListening) { |
|
3371
|
|
|
|
|
3372
|
|
|
// grab scroll container and attach handler |
|
3373
|
|
|
if (ev && this.options.scroll) { |
|
3374
|
|
|
scrollParent = getScrollParent($(ev.target)); |
|
3375
|
|
|
if (!scrollParent.is(window) && !scrollParent.is(document)) { |
|
3376
|
|
|
this.scrollEl = scrollParent; |
|
3377
|
|
|
|
|
3378
|
|
|
// scope to `this`, and use `debounce` to make sure rapid calls don't happen |
|
3379
|
|
|
this.scrollHandlerProxy = debounce($.proxy(this, 'scrollHandler'), 100); |
|
3380
|
|
|
this.scrollEl.on('scroll', this.scrollHandlerProxy); |
|
3381
|
|
|
} |
|
3382
|
|
|
} |
|
3383
|
|
|
|
|
3384
|
|
|
this.computeCoords(); // relies on `scrollEl` |
|
3385
|
|
|
|
|
3386
|
|
|
// get info on the initial cell, date, and coordinates |
|
3387
|
|
|
if (ev) { |
|
3388
|
|
|
cell = this.getCell(ev); |
|
3389
|
|
|
this.origCell = cell; |
|
3390
|
|
|
this.origDate = cell ? cell.date : null; |
|
3391
|
|
|
|
|
3392
|
|
|
this.mouseX0 = ev.pageX; |
|
3393
|
|
|
this.mouseY0 = ev.pageY; |
|
3394
|
|
|
} |
|
3395
|
|
|
|
|
3396
|
|
|
$(document) |
|
3397
|
|
|
.on('mousemove', this.mousemoveProxy = $.proxy(this, 'mousemove')) |
|
3398
|
|
|
.on('mouseup', this.mouseupProxy = $.proxy(this, 'mouseup')) |
|
3399
|
|
|
.on('selectstart', this.preventDefault); // prevents native selection in IE<=8 |
|
3400
|
|
|
|
|
3401
|
|
|
this.isListening = true; |
|
3402
|
|
|
this.trigger('listenStart', ev); |
|
3403
|
|
|
} |
|
3404
|
|
|
}, |
|
3405
|
|
|
|
|
3406
|
|
|
|
|
3407
|
|
|
// Recomputes the drag-critical positions of elements |
|
3408
|
|
|
computeCoords: function() { |
|
3409
|
|
|
this.coordMap.build(); |
|
3410
|
|
|
this.computeScrollBounds(); |
|
3411
|
|
|
}, |
|
3412
|
|
|
|
|
3413
|
|
|
|
|
3414
|
|
|
// Called when the user moves the mouse |
|
3415
|
|
|
mousemove: function(ev) { |
|
3416
|
|
|
var minDistance; |
|
3417
|
|
|
var distanceSq; // current distance from mouseX0/mouseY0, squared |
|
3418
|
|
|
|
|
3419
|
|
|
if (!this.isDragging) { // if not already dragging... |
|
3420
|
|
|
// then start the drag if the minimum distance criteria is met |
|
3421
|
|
|
minDistance = this.options.distance || 1; |
|
3422
|
|
|
distanceSq = Math.pow(ev.pageX - this.mouseX0, 2) + Math.pow(ev.pageY - this.mouseY0, 2); |
|
3423
|
|
|
if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem |
|
3424
|
|
|
this.startDrag(ev); |
|
3425
|
|
|
} |
|
3426
|
|
|
} |
|
3427
|
|
|
|
|
3428
|
|
|
if (this.isDragging) { |
|
3429
|
|
|
this.drag(ev); // report a drag, even if this mousemove initiated the drag |
|
3430
|
|
|
} |
|
3431
|
|
|
}, |
|
3432
|
|
|
|
|
3433
|
|
|
|
|
3434
|
|
|
// Call this to initiate a legitimate drag. |
|
3435
|
|
|
// This function is called internally from this class, but can also be called explicitly from outside |
|
3436
|
|
|
startDrag: function(ev) { |
|
3437
|
|
|
var cell; |
|
3438
|
|
|
|
|
3439
|
|
|
if (!this.isListening) { // startDrag must have manually initiated |
|
3440
|
|
|
this.startListening(); |
|
3441
|
|
|
} |
|
3442
|
|
|
|
|
3443
|
|
|
if (!this.isDragging) { |
|
3444
|
|
|
this.isDragging = true; |
|
3445
|
|
|
this.trigger('dragStart', ev); |
|
3446
|
|
|
|
|
3447
|
|
|
// report the initial cell the mouse is over |
|
3448
|
|
|
cell = this.getCell(ev); |
|
3449
|
|
|
if (cell) { |
|
3450
|
|
|
this.cellOver(cell, true); |
|
3451
|
|
|
} |
|
3452
|
|
|
} |
|
3453
|
|
|
}, |
|
3454
|
|
|
|
|
3455
|
|
|
|
|
3456
|
|
|
// Called while the mouse is being moved and when we know a legitimate drag is taking place |
|
3457
|
|
|
drag: function(ev) { |
|
3458
|
|
|
var cell; |
|
3459
|
|
|
|
|
3460
|
|
|
if (this.isDragging) { |
|
3461
|
|
|
cell = this.getCell(ev); |
|
3462
|
|
|
|
|
3463
|
|
|
if (!isCellsEqual(cell, this.cell)) { // a different cell than before? |
|
3464
|
|
|
if (this.cell) { |
|
3465
|
|
|
this.cellOut(); |
|
3466
|
|
|
} |
|
3467
|
|
|
if (cell) { |
|
3468
|
|
|
this.cellOver(cell); |
|
3469
|
|
|
} |
|
3470
|
|
|
} |
|
3471
|
|
|
|
|
3472
|
|
|
this.dragScroll(ev); // will possibly cause scrolling |
|
3473
|
|
|
} |
|
3474
|
|
|
}, |
|
3475
|
|
|
|
|
3476
|
|
|
|
|
3477
|
|
|
// Called when a the mouse has just moved over a new cell |
|
3478
|
|
|
cellOver: function(cell) { |
|
3479
|
|
|
this.cell = cell; |
|
3480
|
|
|
this.date = cell.date; |
|
3481
|
|
|
this.trigger('cellOver', cell, cell.date); |
|
3482
|
|
|
}, |
|
3483
|
|
|
|
|
3484
|
|
|
|
|
3485
|
|
|
// Called when the mouse has just moved out of a cell |
|
3486
|
|
|
cellOut: function() { |
|
3487
|
|
|
if (this.cell) { |
|
3488
|
|
|
this.trigger('cellOut', this.cell); |
|
3489
|
|
|
this.cell = null; |
|
3490
|
|
|
this.date = null; |
|
3491
|
|
|
} |
|
3492
|
|
|
}, |
|
3493
|
|
|
|
|
3494
|
|
|
|
|
3495
|
|
|
// Called when the user does a mouseup |
|
3496
|
|
|
mouseup: function(ev) { |
|
3497
|
|
|
this.stopDrag(ev); |
|
3498
|
|
|
this.stopListening(ev); |
|
3499
|
|
|
}, |
|
3500
|
|
|
|
|
3501
|
|
|
|
|
3502
|
|
|
// Called when the drag is over. Will not cause listening to stop however. |
|
3503
|
|
|
// A concluding 'cellOut' event will NOT be triggered. |
|
3504
|
|
|
stopDrag: function(ev) { |
|
3505
|
|
|
if (this.isDragging) { |
|
3506
|
|
|
this.stopScrolling(); |
|
3507
|
|
|
this.trigger('dragStop', ev); |
|
3508
|
|
|
this.isDragging = false; |
|
3509
|
|
|
} |
|
3510
|
|
|
}, |
|
3511
|
|
|
|
|
3512
|
|
|
|
|
3513
|
|
|
// Call this to stop listening to the user's mouse events |
|
3514
|
|
|
stopListening: function(ev) { |
|
3515
|
|
|
if (this.isListening) { |
|
3516
|
|
|
|
|
3517
|
|
|
// remove the scroll handler if there is a scrollEl |
|
3518
|
|
|
if (this.scrollEl) { |
|
3519
|
|
|
this.scrollEl.off('scroll', this.scrollHandlerProxy); |
|
3520
|
|
|
this.scrollHandlerProxy = null; |
|
3521
|
|
|
} |
|
3522
|
|
|
|
|
3523
|
|
|
$(document) |
|
3524
|
|
|
.off('mousemove', this.mousemoveProxy) |
|
3525
|
|
|
.off('mouseup', this.mouseupProxy) |
|
3526
|
|
|
.off('selectstart', this.preventDefault); |
|
3527
|
|
|
|
|
3528
|
|
|
this.mousemoveProxy = null; |
|
3529
|
|
|
this.mouseupProxy = null; |
|
3530
|
|
|
|
|
3531
|
|
|
this.isListening = false; |
|
3532
|
|
|
this.trigger('listenStop', ev); |
|
3533
|
|
|
|
|
3534
|
|
|
this.origCell = this.cell = null; |
|
3535
|
|
|
this.origDate = this.date = null; |
|
3536
|
|
|
} |
|
3537
|
|
|
}, |
|
3538
|
|
|
|
|
3539
|
|
|
|
|
3540
|
|
|
// Gets the cell underneath the coordinates for the given mouse event |
|
3541
|
|
|
getCell: function(ev) { |
|
3542
|
|
|
return this.coordMap.getCell(ev.pageX, ev.pageY); |
|
3543
|
|
|
}, |
|
3544
|
|
|
|
|
3545
|
|
|
|
|
3546
|
|
|
// Triggers a callback. Calls a function in the option hash of the same name. |
|
3547
|
|
|
// Arguments beyond the first `name` are forwarded on. |
|
3548
|
|
|
trigger: function(name) { |
|
3549
|
|
|
if (this.options[name]) { |
|
3550
|
|
|
this.options[name].apply(this, Array.prototype.slice.call(arguments, 1)); |
|
3551
|
|
|
} |
|
3552
|
|
|
}, |
|
3553
|
|
|
|
|
3554
|
|
|
|
|
3555
|
|
|
// Stops a given mouse event from doing it's native browser action. In our case, text selection. |
|
3556
|
|
|
preventDefault: function(ev) { |
|
3557
|
|
|
ev.preventDefault(); |
|
3558
|
|
|
}, |
|
3559
|
|
|
|
|
3560
|
|
|
|
|
3561
|
|
|
/* Scrolling |
|
3562
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
3563
|
|
|
|
|
3564
|
|
|
|
|
3565
|
|
|
// Computes and stores the bounding rectangle of scrollEl |
|
3566
|
|
|
computeScrollBounds: function() { |
|
3567
|
|
|
var el = this.scrollEl; |
|
3568
|
|
|
var offset; |
|
3569
|
|
|
|
|
3570
|
|
|
if (el) { |
|
3571
|
|
|
offset = el.offset(); |
|
3572
|
|
|
this.scrollBounds = { |
|
3573
|
|
|
top: offset.top, |
|
3574
|
|
|
left: offset.left, |
|
3575
|
|
|
bottom: offset.top + el.outerHeight(), |
|
3576
|
|
|
right: offset.left + el.outerWidth() |
|
3577
|
|
|
}; |
|
3578
|
|
|
} |
|
3579
|
|
|
}, |
|
3580
|
|
|
|
|
3581
|
|
|
|
|
3582
|
|
|
// Called when the dragging is in progress and scrolling should be updated |
|
3583
|
|
|
dragScroll: function(ev) { |
|
3584
|
|
|
var sensitivity = this.scrollSensitivity; |
|
3585
|
|
|
var bounds = this.scrollBounds; |
|
3586
|
|
|
var topCloseness, bottomCloseness; |
|
3587
|
|
|
var leftCloseness, rightCloseness; |
|
3588
|
|
|
var topVel = 0; |
|
3589
|
|
|
var leftVel = 0; |
|
3590
|
|
|
|
|
3591
|
|
|
if (bounds) { // only scroll if scrollEl exists |
|
3592
|
|
|
|
|
3593
|
|
|
// compute closeness to edges. valid range is from 0.0 - 1.0 |
|
3594
|
|
|
topCloseness = (sensitivity - (ev.pageY - bounds.top)) / sensitivity; |
|
3595
|
|
|
bottomCloseness = (sensitivity - (bounds.bottom - ev.pageY)) / sensitivity; |
|
3596
|
|
|
leftCloseness = (sensitivity - (ev.pageX - bounds.left)) / sensitivity; |
|
3597
|
|
|
rightCloseness = (sensitivity - (bounds.right - ev.pageX)) / sensitivity; |
|
3598
|
|
|
|
|
3599
|
|
|
// translate vertical closeness into velocity. |
|
3600
|
|
|
// mouse must be completely in bounds for velocity to happen. |
|
3601
|
|
|
if (topCloseness >= 0 && topCloseness <= 1) { |
|
3602
|
|
|
topVel = topCloseness * this.scrollSpeed * -1; // negative. for scrolling up |
|
3603
|
|
|
} |
|
3604
|
|
|
else if (bottomCloseness >= 0 && bottomCloseness <= 1) { |
|
3605
|
|
|
topVel = bottomCloseness * this.scrollSpeed; |
|
3606
|
|
|
} |
|
3607
|
|
|
|
|
3608
|
|
|
// translate horizontal closeness into velocity |
|
3609
|
|
|
if (leftCloseness >= 0 && leftCloseness <= 1) { |
|
3610
|
|
|
leftVel = leftCloseness * this.scrollSpeed * -1; // negative. for scrolling left |
|
3611
|
|
|
} |
|
3612
|
|
|
else if (rightCloseness >= 0 && rightCloseness <= 1) { |
|
3613
|
|
|
leftVel = rightCloseness * this.scrollSpeed; |
|
3614
|
|
|
} |
|
3615
|
|
|
} |
|
3616
|
|
|
|
|
3617
|
|
|
this.setScrollVel(topVel, leftVel); |
|
3618
|
|
|
}, |
|
3619
|
|
|
|
|
3620
|
|
|
|
|
3621
|
|
|
// Sets the speed-of-scrolling for the scrollEl |
|
3622
|
|
|
setScrollVel: function(topVel, leftVel) { |
|
3623
|
|
|
|
|
3624
|
|
|
this.scrollTopVel = topVel; |
|
3625
|
|
|
this.scrollLeftVel = leftVel; |
|
3626
|
|
|
|
|
3627
|
|
|
this.constrainScrollVel(); // massages into realistic values |
|
3628
|
|
|
|
|
3629
|
|
|
// if there is non-zero velocity, and an animation loop hasn't already started, then START |
|
3630
|
|
|
if ((this.scrollTopVel || this.scrollLeftVel) && !this.scrollIntervalId) { |
|
3631
|
|
|
this.scrollIntervalId = setInterval( |
|
3632
|
|
|
$.proxy(this, 'scrollIntervalFunc'), // scope to `this` |
|
3633
|
|
|
this.scrollIntervalMs |
|
3634
|
|
|
); |
|
3635
|
|
|
} |
|
3636
|
|
|
}, |
|
3637
|
|
|
|
|
3638
|
|
|
|
|
3639
|
|
|
// Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way |
|
3640
|
|
|
constrainScrollVel: function() { |
|
3641
|
|
|
var el = this.scrollEl; |
|
3642
|
|
|
|
|
3643
|
|
|
if (this.scrollTopVel < 0) { // scrolling up? |
|
3644
|
|
|
if (el.scrollTop() <= 0) { // already scrolled all the way up? |
|
3645
|
|
|
this.scrollTopVel = 0; |
|
3646
|
|
|
} |
|
3647
|
|
|
} |
|
3648
|
|
|
else if (this.scrollTopVel > 0) { // scrolling down? |
|
3649
|
|
|
if (el.scrollTop() + el[0].clientHeight >= el[0].scrollHeight) { // already scrolled all the way down? |
|
3650
|
|
|
this.scrollTopVel = 0; |
|
3651
|
|
|
} |
|
3652
|
|
|
} |
|
3653
|
|
|
|
|
3654
|
|
|
if (this.scrollLeftVel < 0) { // scrolling left? |
|
3655
|
|
|
if (el.scrollLeft() <= 0) { // already scrolled all the left? |
|
3656
|
|
|
this.scrollLeftVel = 0; |
|
3657
|
|
|
} |
|
3658
|
|
|
} |
|
3659
|
|
|
else if (this.scrollLeftVel > 0) { // scrolling right? |
|
3660
|
|
|
if (el.scrollLeft() + el[0].clientWidth >= el[0].scrollWidth) { // already scrolled all the way right? |
|
3661
|
|
|
this.scrollLeftVel = 0; |
|
3662
|
|
|
} |
|
3663
|
|
|
} |
|
3664
|
|
|
}, |
|
3665
|
|
|
|
|
3666
|
|
|
|
|
3667
|
|
|
// This function gets called during every iteration of the scrolling animation loop |
|
3668
|
|
|
scrollIntervalFunc: function() { |
|
3669
|
|
|
var el = this.scrollEl; |
|
3670
|
|
|
var frac = this.scrollIntervalMs / 1000; // considering animation frequency, what the vel should be mult'd by |
|
3671
|
|
|
|
|
3672
|
|
|
// change the value of scrollEl's scroll |
|
3673
|
|
|
if (this.scrollTopVel) { |
|
3674
|
|
|
el.scrollTop(el.scrollTop() + this.scrollTopVel * frac); |
|
3675
|
|
|
} |
|
3676
|
|
|
if (this.scrollLeftVel) { |
|
3677
|
|
|
el.scrollLeft(el.scrollLeft() + this.scrollLeftVel * frac); |
|
3678
|
|
|
} |
|
3679
|
|
|
|
|
3680
|
|
|
this.constrainScrollVel(); // since the scroll values changed, recompute the velocities |
|
3681
|
|
|
|
|
3682
|
|
|
// if scrolled all the way, which causes the vels to be zero, stop the animation loop |
|
3683
|
|
|
if (!this.scrollTopVel && !this.scrollLeftVel) { |
|
3684
|
|
|
this.stopScrolling(); |
|
3685
|
|
|
} |
|
3686
|
|
|
}, |
|
3687
|
|
|
|
|
3688
|
|
|
|
|
3689
|
|
|
// Kills any existing scrolling animation loop |
|
3690
|
|
|
stopScrolling: function() { |
|
3691
|
|
|
if (this.scrollIntervalId) { |
|
3692
|
|
|
clearInterval(this.scrollIntervalId); |
|
3693
|
|
|
this.scrollIntervalId = null; |
|
3694
|
|
|
|
|
3695
|
|
|
// when all done with scrolling, recompute positions since they probably changed |
|
3696
|
|
|
this.computeCoords(); |
|
3697
|
|
|
} |
|
3698
|
|
|
}, |
|
3699
|
|
|
|
|
3700
|
|
|
|
|
3701
|
|
|
// Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce) |
|
3702
|
|
|
scrollHandler: function() { |
|
3703
|
|
|
// recompute all coordinates, but *only* if this is *not* part of our scrolling animation |
|
3704
|
|
|
if (!this.scrollIntervalId) { |
|
3705
|
|
|
this.computeCoords(); |
|
3706
|
|
|
} |
|
3707
|
|
|
} |
|
3708
|
|
|
|
|
3709
|
|
|
}; |
|
3710
|
|
|
|
|
3711
|
|
|
|
|
3712
|
|
|
// Returns `true` if the cells are identically equal. `false` otherwise. |
|
3713
|
|
|
// They must have the same row, col, and be from the same grid. |
|
3714
|
|
|
// Two null values will be considered equal, as two "out of the grid" states are the same. |
|
3715
|
|
|
function isCellsEqual(cell1, cell2) { |
|
3716
|
|
|
|
|
3717
|
|
|
if (!cell1 && !cell2) { |
|
3718
|
|
|
return true; |
|
3719
|
|
|
} |
|
3720
|
|
|
|
|
3721
|
|
|
if (cell1 && cell2) { |
|
3722
|
|
|
return cell1.grid === cell2.grid && |
|
3723
|
|
|
cell1.row === cell2.row && |
|
3724
|
|
|
cell1.col === cell2.col; |
|
3725
|
|
|
} |
|
3726
|
|
|
|
|
3727
|
|
|
return false; |
|
3728
|
|
|
} |
|
3729
|
|
|
|
|
3730
|
|
|
;; |
|
3731
|
|
|
|
|
3732
|
|
|
/* Creates a clone of an element and lets it track the mouse as it moves |
|
3733
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
|
3734
|
|
|
|
|
3735
|
|
|
function MouseFollower(sourceEl, options) { |
|
3736
|
|
|
this.options = options = options || {}; |
|
3737
|
|
|
this.sourceEl = sourceEl; |
|
3738
|
|
|
this.parentEl = options.parentEl ? $(options.parentEl) : sourceEl.parent(); // default to sourceEl's parent |
|
3739
|
|
|
} |
|
3740
|
|
|
|
|
3741
|
|
|
|
|
3742
|
|
|
MouseFollower.prototype = { |
|
3743
|
|
|
|
|
3744
|
|
|
options: null, |
|
3745
|
|
|
|
|
3746
|
|
|
sourceEl: null, // the element that will be cloned and made to look like it is dragging |
|
3747
|
|
|
el: null, // the clone of `sourceEl` that will track the mouse |
|
3748
|
|
|
parentEl: null, // the element that `el` (the clone) will be attached to |
|
3749
|
|
|
|
|
3750
|
|
|
// the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl |
|
3751
|
|
|
top0: null, |
|
3752
|
|
|
left0: null, |
|
3753
|
|
|
|
|
3754
|
|
|
// the initial position of the mouse |
|
3755
|
|
|
mouseY0: null, |
|
3756
|
|
|
mouseX0: null, |
|
3757
|
|
|
|
|
3758
|
|
|
// the number of pixels the mouse has moved from its initial position |
|
3759
|
|
|
topDelta: null, |
|
3760
|
|
|
leftDelta: null, |
|
3761
|
|
|
|
|
3762
|
|
|
mousemoveProxy: null, // document mousemove handler, bound to the MouseFollower's `this` |
|
3763
|
|
|
|
|
3764
|
|
|
isFollowing: false, |
|
3765
|
|
|
isHidden: false, |
|
3766
|
|
|
isAnimating: false, // doing the revert animation? |
|
3767
|
|
|
|
|
3768
|
|
|
|
|
3769
|
|
|
// Causes the element to start following the mouse |
|
3770
|
|
|
start: function(ev) { |
|
3771
|
|
|
if (!this.isFollowing) { |
|
3772
|
|
|
this.isFollowing = true; |
|
3773
|
|
|
|
|
3774
|
|
|
this.mouseY0 = ev.pageY; |
|
3775
|
|
|
this.mouseX0 = ev.pageX; |
|
3776
|
|
|
this.topDelta = 0; |
|
3777
|
|
|
this.leftDelta = 0; |
|
3778
|
|
|
|
|
3779
|
|
|
if (!this.isHidden) { |
|
3780
|
|
|
this.updatePosition(); |
|
3781
|
|
|
} |
|
3782
|
|
|
|
|
3783
|
|
|
$(document).on('mousemove', this.mousemoveProxy = $.proxy(this, 'mousemove')); |
|
3784
|
|
|
} |
|
3785
|
|
|
}, |
|
3786
|
|
|
|
|
3787
|
|
|
|
|
3788
|
|
|
// Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position. |
|
3789
|
|
|
// `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately. |
|
3790
|
|
|
stop: function(shouldRevert, callback) { |
|
3791
|
|
|
var _this = this; |
|
3792
|
|
|
var revertDuration = this.options.revertDuration; |
|
3793
|
|
|
|
|
3794
|
|
|
function complete() { |
|
3795
|
|
|
this.isAnimating = false; |
|
3796
|
|
|
_this.destroyEl(); |
|
3797
|
|
|
|
|
3798
|
|
|
this.top0 = this.left0 = null; // reset state for future updatePosition calls |
|
3799
|
|
|
|
|
3800
|
|
|
if (callback) { |
|
3801
|
|
|
callback(); |
|
3802
|
|
|
} |
|
3803
|
|
|
} |
|
3804
|
|
|
|
|
3805
|
|
|
if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time |
|
3806
|
|
|
this.isFollowing = false; |
|
3807
|
|
|
|
|
3808
|
|
|
$(document).off('mousemove', this.mousemoveProxy); |
|
3809
|
|
|
|
|
3810
|
|
|
if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation? |
|
3811
|
|
|
this.isAnimating = true; |
|
3812
|
|
|
this.el.animate({ |
|
3813
|
|
|
top: this.top0, |
|
3814
|
|
|
left: this.left0 |
|
3815
|
|
|
}, { |
|
3816
|
|
|
duration: revertDuration, |
|
3817
|
|
|
complete: complete |
|
3818
|
|
|
}); |
|
3819
|
|
|
} |
|
3820
|
|
|
else { |
|
3821
|
|
|
complete(); |
|
3822
|
|
|
} |
|
3823
|
|
|
} |
|
3824
|
|
|
}, |
|
3825
|
|
|
|
|
3826
|
|
|
|
|
3827
|
|
|
// Gets the tracking element. Create it if necessary |
|
3828
|
|
|
getEl: function() { |
|
3829
|
|
|
var el = this.el; |
|
3830
|
|
|
|
|
3831
|
|
|
if (!el) { |
|
3832
|
|
|
this.sourceEl.width(); // hack to force IE8 to compute correct bounding box |
|
3833
|
|
|
el = this.el = this.sourceEl.clone() |
|
3834
|
|
|
.css({ |
|
3835
|
|
|
position: 'absolute', |
|
3836
|
|
|
visibility: '', // in case original element was hidden (commonly through hideEvents()) |
|
3837
|
|
|
display: this.isHidden ? 'none' : '', // for when initially hidden |
|
3838
|
|
|
margin: 0, |
|
3839
|
|
|
right: 'auto', // erase and set width instead |
|
3840
|
|
|
bottom: 'auto', // erase and set height instead |
|
3841
|
|
|
width: this.sourceEl.width(), // explicit height in case there was a 'right' value |
|
3842
|
|
|
height: this.sourceEl.height(), // explicit width in case there was a 'bottom' value |
|
3843
|
|
|
opacity: this.options.opacity || '', |
|
3844
|
|
|
zIndex: this.options.zIndex |
|
3845
|
|
|
}) |
|
3846
|
|
|
.appendTo(this.parentEl); |
|
3847
|
|
|
} |
|
3848
|
|
|
|
|
3849
|
|
|
return el; |
|
3850
|
|
|
}, |
|
3851
|
|
|
|
|
3852
|
|
|
|
|
3853
|
|
|
// Removes the tracking element if it has already been created |
|
3854
|
|
|
destroyEl: function() { |
|
3855
|
|
|
if (this.el) { |
|
3856
|
|
|
this.el.remove(); |
|
3857
|
|
|
this.el = null; |
|
3858
|
|
|
} |
|
3859
|
|
|
}, |
|
3860
|
|
|
|
|
3861
|
|
|
|
|
3862
|
|
|
// Update the CSS position of the tracking element |
|
3863
|
|
|
updatePosition: function() { |
|
3864
|
|
|
var sourceOffset; |
|
3865
|
|
|
var origin; |
|
3866
|
|
|
|
|
3867
|
|
|
this.getEl(); // ensure this.el |
|
3868
|
|
|
|
|
3869
|
|
|
// make sure origin info was computed |
|
3870
|
|
|
if (this.top0 === null) { |
|
3871
|
|
|
this.sourceEl.width(); // hack to force IE8 to compute correct bounding box |
|
3872
|
|
|
sourceOffset = this.sourceEl.offset(); |
|
3873
|
|
|
origin = this.el.offsetParent().offset(); |
|
3874
|
|
|
this.top0 = sourceOffset.top - origin.top; |
|
3875
|
|
|
this.left0 = sourceOffset.left - origin.left; |
|
3876
|
|
|
} |
|
3877
|
|
|
|
|
3878
|
|
|
this.el.css({ |
|
3879
|
|
|
top: this.top0 + this.topDelta, |
|
3880
|
|
|
left: this.left0 + this.leftDelta |
|
3881
|
|
|
}); |
|
3882
|
|
|
}, |
|
3883
|
|
|
|
|
3884
|
|
|
|
|
3885
|
|
|
// Gets called when the user moves the mouse |
|
3886
|
|
|
mousemove: function(ev) { |
|
3887
|
|
|
this.topDelta = ev.pageY - this.mouseY0; |
|
3888
|
|
|
this.leftDelta = ev.pageX - this.mouseX0; |
|
3889
|
|
|
|
|
3890
|
|
|
if (!this.isHidden) { |
|
3891
|
|
|
this.updatePosition(); |
|
3892
|
|
|
} |
|
3893
|
|
|
}, |
|
3894
|
|
|
|
|
3895
|
|
|
|
|
3896
|
|
|
// Temporarily makes the tracking element invisible. Can be called before following starts |
|
3897
|
|
|
hide: function() { |
|
3898
|
|
|
if (!this.isHidden) { |
|
3899
|
|
|
this.isHidden = true; |
|
3900
|
|
|
if (this.el) { |
|
3901
|
|
|
this.el.hide(); |
|
3902
|
|
|
} |
|
3903
|
|
|
} |
|
3904
|
|
|
}, |
|
3905
|
|
|
|
|
3906
|
|
|
|
|
3907
|
|
|
// Show the tracking element after it has been temporarily hidden |
|
3908
|
|
|
show: function() { |
|
3909
|
|
|
if (this.isHidden) { |
|
3910
|
|
|
this.isHidden = false; |
|
3911
|
|
|
this.updatePosition(); |
|
3912
|
|
|
this.getEl().show(); |
|
3913
|
|
|
} |
|
3914
|
|
|
} |
|
3915
|
|
|
|
|
3916
|
|
|
}; |
|
3917
|
|
|
|
|
3918
|
|
|
;; |
|
3919
|
|
|
|
|
3920
|
|
|
/* A utility class for rendering <tr> rows. |
|
3921
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
|
3922
|
|
|
// It leverages methods of the subclass and the View to determine custom rendering behavior for each row "type" |
|
3923
|
|
|
// (such as highlight rows, day rows, helper rows, etc). |
|
3924
|
|
|
|
|
3925
|
|
|
function RowRenderer(view) { |
|
3926
|
|
|
this.view = view; |
|
3927
|
|
|
} |
|
3928
|
|
|
|
|
3929
|
|
|
|
|
3930
|
|
|
RowRenderer.prototype = { |
|
3931
|
|
|
|
|
3932
|
|
|
view: null, // a View object |
|
3933
|
|
|
cellHtml: '<td/>', // plain default HTML used for a cell when no other is available |
|
3934
|
|
|
|
|
3935
|
|
|
|
|
3936
|
|
|
// Renders the HTML for a row, leveraging custom cell-HTML-renderers based on the `rowType`. |
|
3937
|
|
|
// Also applies the "intro" and "outro" cells, which are specified by the subclass and views. |
|
3938
|
|
|
// `row` is an optional row number. |
|
3939
|
|
|
rowHtml: function(rowType, row) { |
|
3940
|
|
|
var view = this.view; |
|
3941
|
|
|
var renderCell = this.getHtmlRenderer('cell', rowType); |
|
3942
|
|
|
var cellHtml = ''; |
|
3943
|
|
|
var col; |
|
3944
|
|
|
var date; |
|
3945
|
|
|
|
|
3946
|
|
|
row = row || 0; |
|
3947
|
|
|
|
|
3948
|
|
|
for (col = 0; col < view.colCnt; col++) { |
|
3949
|
|
|
date = view.cellToDate(row, col); |
|
3950
|
|
|
cellHtml += renderCell(row, col, date); |
|
3951
|
|
|
} |
|
3952
|
|
|
|
|
3953
|
|
|
cellHtml = this.bookendCells(cellHtml, rowType, row); // apply intro and outro |
|
3954
|
|
|
|
|
3955
|
|
|
return '<tr>' + cellHtml + '</tr>'; |
|
3956
|
|
|
}, |
|
3957
|
|
|
|
|
3958
|
|
|
|
|
3959
|
|
|
// Applies the "intro" and "outro" HTML to the given cells. |
|
3960
|
|
|
// Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro. |
|
3961
|
|
|
// `cells` can be an HTML string of <td>'s or a jQuery <tr> element |
|
3962
|
|
|
// `row` is an optional row number. |
|
3963
|
|
|
bookendCells: function(cells, rowType, row) { |
|
3964
|
|
|
var view = this.view; |
|
3965
|
|
|
var intro = this.getHtmlRenderer('intro', rowType)(row || 0); |
|
3966
|
|
|
var outro = this.getHtmlRenderer('outro', rowType)(row || 0); |
|
3967
|
|
|
var isRTL = view.opt('isRTL'); |
|
3968
|
|
|
var prependHtml = isRTL ? outro : intro; |
|
3969
|
|
|
var appendHtml = isRTL ? intro : outro; |
|
3970
|
|
|
|
|
3971
|
|
|
if (typeof cells === 'string') { |
|
3972
|
|
|
return prependHtml + cells + appendHtml; |
|
3973
|
|
|
} |
|
3974
|
|
|
else { // a jQuery <tr> element |
|
3975
|
|
|
return cells.prepend(prependHtml).append(appendHtml); |
|
3976
|
|
|
} |
|
3977
|
|
|
}, |
|
3978
|
|
|
|
|
3979
|
|
|
|
|
3980
|
|
|
// Returns an HTML-rendering function given a specific `rendererName` (like cell, intro, or outro) and a specific |
|
3981
|
|
|
// `rowType` (like day, eventSkeleton, helperSkeleton), which is optional. |
|
3982
|
|
|
// If a renderer for the specific rowType doesn't exist, it will fall back to a generic renderer. |
|
3983
|
|
|
// We will query the View object first for any custom rendering functions, then the methods of the subclass. |
|
3984
|
|
|
getHtmlRenderer: function(rendererName, rowType) { |
|
3985
|
|
|
var view = this.view; |
|
3986
|
|
|
var generalName; // like "cellHtml" |
|
3987
|
|
|
var specificName; // like "dayCellHtml". based on rowType |
|
3988
|
|
|
var provider; // either the View or the RowRenderer subclass, whichever provided the method |
|
3989
|
|
|
var renderer; |
|
3990
|
|
|
|
|
3991
|
|
|
generalName = rendererName + 'Html'; |
|
3992
|
|
|
if (rowType) { |
|
3993
|
|
|
specificName = rowType + capitaliseFirstLetter(rendererName) + 'Html'; |
|
3994
|
|
|
} |
|
3995
|
|
|
|
|
3996
|
|
|
if (specificName && (renderer = view[specificName])) { |
|
3997
|
|
|
provider = view; |
|
3998
|
|
|
} |
|
3999
|
|
|
else if (specificName && (renderer = this[specificName])) { |
|
4000
|
|
|
provider = this; |
|
4001
|
|
|
} |
|
4002
|
|
|
else if ((renderer = view[generalName])) { |
|
4003
|
|
|
provider = view; |
|
4004
|
|
|
} |
|
4005
|
|
|
else if ((renderer = this[generalName])) { |
|
4006
|
|
|
provider = this; |
|
4007
|
|
|
} |
|
4008
|
|
|
|
|
4009
|
|
|
if (typeof renderer === 'function') { |
|
4010
|
|
|
return function(row) { |
|
4011
|
|
|
return renderer.apply(provider, arguments) || ''; // use correct `this` and always return a string |
|
|
|
|
|
|
4012
|
|
|
}; |
|
4013
|
|
|
} |
|
4014
|
|
|
|
|
4015
|
|
|
// the rendered can be a plain string as well. if not specified, always an empty string. |
|
4016
|
|
|
return function() { |
|
4017
|
|
|
return renderer || ''; |
|
4018
|
|
|
}; |
|
4019
|
|
|
} |
|
4020
|
|
|
|
|
4021
|
|
|
}; |
|
4022
|
|
|
|
|
4023
|
|
|
;; |
|
4024
|
|
|
|
|
4025
|
|
|
/* An abstract class comprised of a "grid" of cells that each represent a specific datetime |
|
4026
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
|
4027
|
|
|
|
|
4028
|
|
|
function Grid(view) { |
|
4029
|
|
|
RowRenderer.call(this, view); // call the super-constructor |
|
4030
|
|
|
this.coordMap = new GridCoordMap(this); |
|
4031
|
|
|
} |
|
4032
|
|
|
|
|
4033
|
|
|
|
|
4034
|
|
|
Grid.prototype = createObject(RowRenderer.prototype); // declare the super-class |
|
4035
|
|
|
$.extend(Grid.prototype, { |
|
4036
|
|
|
|
|
4037
|
|
|
el: null, // the containing element |
|
4038
|
|
|
coordMap: null, // a GridCoordMap that converts pixel values to datetimes |
|
4039
|
|
|
cellDuration: null, // a cell's duration. subclasses must assign this ASAP |
|
4040
|
|
|
|
|
4041
|
|
|
|
|
4042
|
|
|
// Renders the grid into the `el` element. |
|
4043
|
|
|
// Subclasses should override and call this super-method when done. |
|
4044
|
|
|
render: function() { |
|
4045
|
|
|
this.bindHandlers(); |
|
4046
|
|
|
}, |
|
4047
|
|
|
|
|
4048
|
|
|
|
|
4049
|
|
|
// Called when the grid's resources need to be cleaned up |
|
4050
|
|
|
destroy: function() { |
|
4051
|
|
|
// subclasses can implement |
|
4052
|
|
|
}, |
|
4053
|
|
|
|
|
4054
|
|
|
|
|
4055
|
|
|
/* Coordinates & Cells |
|
4056
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
4057
|
|
|
|
|
4058
|
|
|
|
|
4059
|
|
|
// Populates the given empty arrays with the y and x coordinates of the cells |
|
4060
|
|
|
buildCoords: function(rows, cols) { |
|
4061
|
|
|
// subclasses must implement |
|
4062
|
|
|
}, |
|
4063
|
|
|
|
|
4064
|
|
|
|
|
4065
|
|
|
// Given a cell object, returns the date for that cell |
|
4066
|
|
|
getCellDate: function(cell) { |
|
4067
|
|
|
// subclasses must implement |
|
4068
|
|
|
}, |
|
4069
|
|
|
|
|
4070
|
|
|
|
|
4071
|
|
|
// Given a cell object, returns the element that represents the cell's whole-day |
|
4072
|
|
|
getCellDayEl: function(cell) { |
|
4073
|
|
|
// subclasses must implement |
|
4074
|
|
|
}, |
|
4075
|
|
|
|
|
4076
|
|
|
|
|
4077
|
|
|
// Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects |
|
4078
|
|
|
rangeToSegs: function(start, end) { |
|
4079
|
|
|
// subclasses must implement |
|
4080
|
|
|
}, |
|
4081
|
|
|
|
|
4082
|
|
|
|
|
4083
|
|
|
/* Handlers |
|
4084
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
4085
|
|
|
|
|
4086
|
|
|
|
|
4087
|
|
|
// Attach handlers to `this.el`, using bubbling to listen to all ancestors. |
|
4088
|
|
|
// We don't need to undo any of this in a "destroy" method, because the view will simply remove `this.el` from the |
|
4089
|
|
|
// DOM and jQuery will be smart enough to garbage collect the handlers. |
|
4090
|
|
|
bindHandlers: function() { |
|
4091
|
|
|
var _this = this; |
|
4092
|
|
|
|
|
4093
|
|
|
this.el.on('mousedown', function(ev) { |
|
4094
|
|
|
if ( |
|
4095
|
|
|
!$(ev.target).is('.fc-event-container *, .fc-more') && // not an an event element, or "more.." link |
|
4096
|
|
|
!$(ev.target).closest('.fc-popover').length // not on a popover (like the "more.." events one) |
|
4097
|
|
|
) { |
|
4098
|
|
|
_this.dayMousedown(ev); |
|
4099
|
|
|
} |
|
4100
|
|
|
}); |
|
4101
|
|
|
|
|
4102
|
|
|
this.bindSegHandlers(); // attach event-element-related handlers. in Grid.events.js |
|
4103
|
|
|
}, |
|
4104
|
|
|
|
|
4105
|
|
|
|
|
4106
|
|
|
// Process a mousedown on an element that represents a day. For day clicking and selecting. |
|
4107
|
|
|
dayMousedown: function(ev) { |
|
4108
|
|
|
var _this = this; |
|
4109
|
|
|
var view = this.view; |
|
4110
|
|
|
var isSelectable = view.opt('selectable'); |
|
4111
|
|
|
var dates = null; // the inclusive dates of the selection. will be null if no selection |
|
4112
|
|
|
var start; // the inclusive start of the selection |
|
4113
|
|
|
var end; // the *exclusive* end of the selection |
|
4114
|
|
|
var dayEl; |
|
4115
|
|
|
|
|
4116
|
|
|
// this listener tracks a mousedown on a day element, and a subsequent drag. |
|
4117
|
|
|
// if the drag ends on the same day, it is a 'dayClick'. |
|
4118
|
|
|
// if 'selectable' is enabled, this listener also detects selections. |
|
4119
|
|
|
var dragListener = new DragListener(this.coordMap, { |
|
4120
|
|
|
//distance: 5, // needs more work if we want dayClick to fire correctly |
|
4121
|
|
|
scroll: view.opt('dragScroll'), |
|
4122
|
|
|
dragStart: function() { |
|
4123
|
|
|
view.unselect(); // since we could be rendering a new selection, we want to clear any old one |
|
4124
|
|
|
}, |
|
4125
|
|
|
cellOver: function(cell, date) { |
|
4126
|
|
|
if (dragListener.origDate) { // click needs to have started on a cell |
|
4127
|
|
|
|
|
4128
|
|
|
dayEl = _this.getCellDayEl(cell); |
|
4129
|
|
|
|
|
4130
|
|
|
dates = [ date, dragListener.origDate ].sort(dateCompare); |
|
4131
|
|
|
start = dates[0]; |
|
4132
|
|
|
end = dates[1].clone().add(_this.cellDuration); |
|
4133
|
|
|
|
|
4134
|
|
|
if (isSelectable) { |
|
4135
|
|
|
_this.renderSelection(start, end); |
|
4136
|
|
|
} |
|
4137
|
|
|
} |
|
4138
|
|
|
}, |
|
4139
|
|
|
cellOut: function(cell, date) { |
|
4140
|
|
|
dates = null; |
|
4141
|
|
|
_this.destroySelection(); |
|
4142
|
|
|
}, |
|
4143
|
|
|
listenStop: function(ev) { |
|
4144
|
|
|
if (dates) { // started and ended on a cell? |
|
4145
|
|
|
if (dates[0].isSame(dates[1])) { |
|
4146
|
|
|
view.trigger('dayClick', dayEl[0], start, ev); |
|
4147
|
|
|
} |
|
4148
|
|
|
if (isSelectable) { |
|
4149
|
|
|
// the selection will already have been rendered. just report it |
|
4150
|
|
|
view.reportSelection(start, end, ev); |
|
4151
|
|
|
} |
|
4152
|
|
|
} |
|
4153
|
|
|
} |
|
4154
|
|
|
}); |
|
4155
|
|
|
|
|
4156
|
|
|
dragListener.mousedown(ev); // start listening, which will eventually initiate a dragStart |
|
4157
|
|
|
}, |
|
4158
|
|
|
|
|
4159
|
|
|
|
|
4160
|
|
|
/* Event Dragging |
|
4161
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
4162
|
|
|
|
|
4163
|
|
|
|
|
4164
|
|
|
// Renders a visual indication of a event being dragged over the given date(s). |
|
4165
|
|
|
// `end` can be null, as well as `seg`. See View's documentation on renderDrag for more info. |
|
4166
|
|
|
// A returned value of `true` signals that a mock "helper" event has been rendered. |
|
4167
|
|
|
renderDrag: function(start, end, seg) { |
|
4168
|
|
|
// subclasses must implement |
|
4169
|
|
|
}, |
|
4170
|
|
|
|
|
4171
|
|
|
|
|
4172
|
|
|
// Unrenders a visual indication of an event being dragged |
|
4173
|
|
|
destroyDrag: function() { |
|
4174
|
|
|
// subclasses must implement |
|
4175
|
|
|
}, |
|
4176
|
|
|
|
|
4177
|
|
|
|
|
4178
|
|
|
/* Event Resizing |
|
4179
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
4180
|
|
|
|
|
4181
|
|
|
|
|
4182
|
|
|
// Renders a visual indication of an event being resized. |
|
4183
|
|
|
// `start` and `end` are the updated dates of the event. `seg` is the original segment object involved in the drag. |
|
4184
|
|
|
renderResize: function(start, end, seg) { |
|
4185
|
|
|
// subclasses must implement |
|
4186
|
|
|
}, |
|
4187
|
|
|
|
|
4188
|
|
|
|
|
4189
|
|
|
// Unrenders a visual indication of an event being resized. |
|
4190
|
|
|
destroyResize: function() { |
|
4191
|
|
|
// subclasses must implement |
|
4192
|
|
|
}, |
|
4193
|
|
|
|
|
4194
|
|
|
|
|
4195
|
|
|
/* Event Helper |
|
4196
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
4197
|
|
|
|
|
4198
|
|
|
|
|
4199
|
|
|
// Renders a mock event over the given date(s). |
|
4200
|
|
|
// `end` can be null, in which case the mock event that is rendered will have a null end time. |
|
4201
|
|
|
// `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging. |
|
4202
|
|
|
renderRangeHelper: function(start, end, sourceSeg) { |
|
4203
|
|
|
var view = this.view; |
|
4204
|
|
|
var fakeEvent; |
|
4205
|
|
|
|
|
4206
|
|
|
// compute the end time if forced to do so (this is what EventManager does) |
|
4207
|
|
|
if (!end && view.opt('forceEventDuration')) { |
|
4208
|
|
|
end = view.calendar.getDefaultEventEnd(!start.hasTime(), start); |
|
4209
|
|
|
} |
|
4210
|
|
|
|
|
4211
|
|
|
fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible |
|
4212
|
|
|
fakeEvent.start = start; |
|
4213
|
|
|
fakeEvent.end = end; |
|
4214
|
|
|
fakeEvent.allDay = !(start.hasTime() || (end && end.hasTime())); // freshly compute allDay |
|
4215
|
|
|
|
|
4216
|
|
|
// this extra className will be useful for differentiating real events from mock events in CSS |
|
4217
|
|
|
fakeEvent.className = (fakeEvent.className || []).concat('fc-helper'); |
|
4218
|
|
|
|
|
4219
|
|
|
// if something external is being dragged in, don't render a resizer |
|
4220
|
|
|
if (!sourceSeg) { |
|
4221
|
|
|
fakeEvent.editable = false; |
|
4222
|
|
|
} |
|
4223
|
|
|
|
|
4224
|
|
|
this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering |
|
4225
|
|
|
}, |
|
4226
|
|
|
|
|
4227
|
|
|
|
|
4228
|
|
|
// Renders a mock event |
|
4229
|
|
|
renderHelper: function(event, sourceSeg) { |
|
4230
|
|
|
// subclasses must implement |
|
4231
|
|
|
}, |
|
4232
|
|
|
|
|
4233
|
|
|
|
|
4234
|
|
|
// Unrenders a mock event |
|
4235
|
|
|
destroyHelper: function() { |
|
4236
|
|
|
// subclasses must implement |
|
4237
|
|
|
}, |
|
4238
|
|
|
|
|
4239
|
|
|
|
|
4240
|
|
|
/* Selection |
|
4241
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
4242
|
|
|
|
|
4243
|
|
|
|
|
4244
|
|
|
// Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses. |
|
4245
|
|
|
renderSelection: function(start, end) { |
|
4246
|
|
|
this.renderHighlight(start, end); |
|
4247
|
|
|
}, |
|
4248
|
|
|
|
|
4249
|
|
|
|
|
4250
|
|
|
// Unrenders any visual indications of a selection. Will unrender a highlight by default. |
|
4251
|
|
|
destroySelection: function() { |
|
4252
|
|
|
this.destroyHighlight(); |
|
4253
|
|
|
}, |
|
4254
|
|
|
|
|
4255
|
|
|
|
|
4256
|
|
|
/* Highlight |
|
4257
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
4258
|
|
|
|
|
4259
|
|
|
|
|
4260
|
|
|
// Puts visual emphasis on a certain date range |
|
4261
|
|
|
renderHighlight: function(start, end) { |
|
4262
|
|
|
// subclasses should implement |
|
4263
|
|
|
}, |
|
4264
|
|
|
|
|
4265
|
|
|
|
|
4266
|
|
|
// Removes visual emphasis on a date range |
|
4267
|
|
|
destroyHighlight: function() { |
|
4268
|
|
|
// subclasses should implement |
|
4269
|
|
|
}, |
|
4270
|
|
|
|
|
4271
|
|
|
|
|
4272
|
|
|
|
|
4273
|
|
|
/* Generic rendering utilities for subclasses |
|
4274
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
4275
|
|
|
|
|
4276
|
|
|
|
|
4277
|
|
|
// Renders a day-of-week header row |
|
4278
|
|
|
headHtml: function() { |
|
4279
|
|
|
return '' + |
|
4280
|
|
|
'<div class="fc-row ' + this.view.widgetHeaderClass + '">' + |
|
4281
|
|
|
'<table>' + |
|
4282
|
|
|
'<thead>' + |
|
4283
|
|
|
this.rowHtml('head') + // leverages RowRenderer |
|
4284
|
|
|
'</thead>' + |
|
4285
|
|
|
'</table>' + |
|
4286
|
|
|
'</div>'; |
|
4287
|
|
|
}, |
|
4288
|
|
|
|
|
4289
|
|
|
|
|
4290
|
|
|
// Used by the `headHtml` method, via RowRenderer, for rendering the HTML of a day-of-week header cell |
|
4291
|
|
|
headCellHtml: function(row, col, date) { |
|
4292
|
|
|
var view = this.view; |
|
4293
|
|
|
var calendar = view.calendar; |
|
4294
|
|
|
var colFormat = view.opt('columnFormat'); |
|
4295
|
|
|
|
|
4296
|
|
|
return '' + |
|
4297
|
|
|
'<th class="fc-day-header ' + view.widgetHeaderClass + ' fc-' + dayIDs[date.day()] + '">' + |
|
4298
|
|
|
htmlEscape(calendar.formatDate(date, colFormat)) + |
|
4299
|
|
|
'</th>'; |
|
4300
|
|
|
}, |
|
4301
|
|
|
|
|
4302
|
|
|
|
|
4303
|
|
|
// Renders the HTML for a single-day background cell |
|
4304
|
|
|
bgCellHtml: function(row, col, date) { |
|
4305
|
|
|
var view = this.view; |
|
4306
|
|
|
var classes = this.getDayClasses(date); |
|
4307
|
|
|
|
|
4308
|
|
|
classes.unshift('fc-day', view.widgetContentClass); |
|
4309
|
|
|
|
|
4310
|
|
|
return '<td class="' + classes.join(' ') + '" data-date="' + date.format() + '"></td>'; |
|
4311
|
|
|
}, |
|
4312
|
|
|
|
|
4313
|
|
|
|
|
4314
|
|
|
// Computes HTML classNames for a single-day cell |
|
4315
|
|
|
getDayClasses: function(date) { |
|
4316
|
|
|
var view = this.view; |
|
4317
|
|
|
var today = view.calendar.getNow().stripTime(); |
|
4318
|
|
|
var classes = [ 'fc-' + dayIDs[date.day()] ]; |
|
4319
|
|
|
|
|
4320
|
|
|
if ( |
|
4321
|
|
|
view.name === 'month' && |
|
4322
|
|
|
date.month() != view.intervalStart.month() |
|
4323
|
|
|
) { |
|
4324
|
|
|
classes.push('fc-other-month'); |
|
4325
|
|
|
} |
|
4326
|
|
|
|
|
4327
|
|
|
if (date.isSame(today, 'day')) { |
|
4328
|
|
|
classes.push( |
|
4329
|
|
|
'fc-today', |
|
4330
|
|
|
view.highlightStateClass |
|
4331
|
|
|
); |
|
4332
|
|
|
} |
|
4333
|
|
|
else if (date < today) { |
|
4334
|
|
|
classes.push('fc-past'); |
|
4335
|
|
|
} |
|
4336
|
|
|
else { |
|
4337
|
|
|
classes.push('fc-future'); |
|
4338
|
|
|
} |
|
4339
|
|
|
|
|
4340
|
|
|
return classes; |
|
4341
|
|
|
} |
|
4342
|
|
|
|
|
4343
|
|
|
}); |
|
4344
|
|
|
|
|
4345
|
|
|
;; |
|
4346
|
|
|
|
|
4347
|
|
|
/* Event-rendering and event-interaction methods for the abstract Grid class |
|
4348
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
|
4349
|
|
|
|
|
4350
|
|
|
$.extend(Grid.prototype, { |
|
4351
|
|
|
|
|
4352
|
|
|
mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing |
|
4353
|
|
|
isDraggingSeg: false, // is a segment being dragged? boolean |
|
4354
|
|
|
isResizingSeg: false, // is a segment being resized? boolean |
|
4355
|
|
|
|
|
4356
|
|
|
|
|
4357
|
|
|
// Renders the given events onto the grid |
|
4358
|
|
|
renderEvents: function(events) { |
|
4359
|
|
|
// subclasses must implement |
|
4360
|
|
|
}, |
|
4361
|
|
|
|
|
4362
|
|
|
|
|
4363
|
|
|
// Retrieves all rendered segment objects in this grid |
|
4364
|
|
|
getSegs: function() { |
|
4365
|
|
|
// subclasses must implement |
|
4366
|
|
|
}, |
|
4367
|
|
|
|
|
4368
|
|
|
|
|
4369
|
|
|
// Unrenders all events. Subclasses should implement, calling this super-method first. |
|
4370
|
|
|
destroyEvents: function() { |
|
4371
|
|
|
this.triggerSegMouseout(); // trigger an eventMouseout if user's mouse is over an event |
|
4372
|
|
|
}, |
|
4373
|
|
|
|
|
4374
|
|
|
|
|
4375
|
|
|
// Renders a `el` property for each seg, and only returns segments that successfully rendered |
|
4376
|
|
|
renderSegs: function(segs, disableResizing) { |
|
4377
|
|
|
var view = this.view; |
|
4378
|
|
|
var html = ''; |
|
4379
|
|
|
var renderedSegs = []; |
|
4380
|
|
|
var i; |
|
4381
|
|
|
|
|
4382
|
|
|
// build a large concatenation of event segment HTML |
|
4383
|
|
|
for (i = 0; i < segs.length; i++) { |
|
4384
|
|
|
html += this.renderSegHtml(segs[i], disableResizing); |
|
4385
|
|
|
} |
|
4386
|
|
|
|
|
4387
|
|
|
// Grab individual elements from the combined HTML string. Use each as the default rendering. |
|
4388
|
|
|
// Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false. |
|
4389
|
|
|
$(html).each(function(i, node) { |
|
4390
|
|
|
var seg = segs[i]; |
|
4391
|
|
|
var el = view.resolveEventEl(seg.event, $(node)); |
|
4392
|
|
|
if (el) { |
|
4393
|
|
|
el.data('fc-seg', seg); // used by handlers |
|
4394
|
|
|
seg.el = el; |
|
4395
|
|
|
renderedSegs.push(seg); |
|
4396
|
|
|
} |
|
4397
|
|
|
}); |
|
4398
|
|
|
|
|
4399
|
|
|
return renderedSegs; |
|
4400
|
|
|
}, |
|
4401
|
|
|
|
|
4402
|
|
|
|
|
4403
|
|
|
// Generates the HTML for the default rendering of a segment |
|
4404
|
|
|
renderSegHtml: function(seg, disableResizing) { |
|
4405
|
|
|
// subclasses must implement |
|
4406
|
|
|
}, |
|
4407
|
|
|
|
|
4408
|
|
|
|
|
4409
|
|
|
// Converts an array of event objects into an array of segment objects |
|
4410
|
|
|
eventsToSegs: function(events, intervalStart, intervalEnd) { |
|
4411
|
|
|
var _this = this; |
|
4412
|
|
|
|
|
4413
|
|
|
return $.map(events, function(event) { |
|
4414
|
|
|
return _this.eventToSegs(event, intervalStart, intervalEnd); // $.map flattens all returned arrays together |
|
4415
|
|
|
}); |
|
4416
|
|
|
}, |
|
4417
|
|
|
|
|
4418
|
|
|
|
|
4419
|
|
|
// Slices a single event into an array of event segments. |
|
4420
|
|
|
// When `intervalStart` and `intervalEnd` are specified, intersect the events with that interval. |
|
4421
|
|
|
// Otherwise, let the subclass decide how it wants to slice the segments over the grid. |
|
4422
|
|
|
eventToSegs: function(event, intervalStart, intervalEnd) { |
|
4423
|
|
|
var eventStart = event.start.clone().stripZone(); // normalize |
|
4424
|
|
|
var eventEnd = this.view.calendar.getEventEnd(event).stripZone(); // compute (if necessary) and normalize |
|
4425
|
|
|
var segs; |
|
4426
|
|
|
var i, seg; |
|
4427
|
|
|
|
|
4428
|
|
|
if (intervalStart && intervalEnd) { |
|
4429
|
|
|
seg = intersectionToSeg(eventStart, eventEnd, intervalStart, intervalEnd); |
|
4430
|
|
|
segs = seg ? [ seg ] : []; |
|
4431
|
|
|
} |
|
4432
|
|
|
else { |
|
4433
|
|
|
segs = this.rangeToSegs(eventStart, eventEnd); // defined by the subclass |
|
4434
|
|
|
} |
|
4435
|
|
|
|
|
4436
|
|
|
// assign extra event-related properties to the segment objects |
|
4437
|
|
|
for (i = 0; i < segs.length; i++) { |
|
4438
|
|
|
seg = segs[i]; |
|
4439
|
|
|
seg.event = event; |
|
4440
|
|
|
seg.eventStartMS = +eventStart; |
|
4441
|
|
|
seg.eventDurationMS = eventEnd - eventStart; |
|
4442
|
|
|
} |
|
4443
|
|
|
|
|
4444
|
|
|
return segs; |
|
4445
|
|
|
}, |
|
4446
|
|
|
|
|
4447
|
|
|
|
|
4448
|
|
|
/* Handlers |
|
4449
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
4450
|
|
|
|
|
4451
|
|
|
|
|
4452
|
|
|
// Attaches event-element-related handlers to the container element and leverage bubbling |
|
4453
|
|
|
bindSegHandlers: function() { |
|
4454
|
|
|
var _this = this; |
|
4455
|
|
|
var view = this.view; |
|
4456
|
|
|
|
|
4457
|
|
|
$.each( |
|
4458
|
|
|
{ |
|
4459
|
|
|
mouseenter: function(seg, ev) { |
|
4460
|
|
|
_this.triggerSegMouseover(seg, ev); |
|
4461
|
|
|
}, |
|
4462
|
|
|
mouseleave: function(seg, ev) { |
|
4463
|
|
|
_this.triggerSegMouseout(seg, ev); |
|
4464
|
|
|
}, |
|
4465
|
|
|
click: function(seg, ev) { |
|
4466
|
|
|
return view.trigger('eventClick', this, seg.event, ev); // can return `false` to cancel |
|
4467
|
|
|
}, |
|
4468
|
|
|
mousedown: function(seg, ev) { |
|
4469
|
|
|
if ($(ev.target).is('.fc-resizer') && view.isEventResizable(seg.event)) { |
|
4470
|
|
|
_this.segResizeMousedown(seg, ev); |
|
4471
|
|
|
} |
|
4472
|
|
|
else if (view.isEventDraggable(seg.event)) { |
|
4473
|
|
|
_this.segDragMousedown(seg, ev); |
|
4474
|
|
|
} |
|
4475
|
|
|
} |
|
4476
|
|
|
}, |
|
4477
|
|
|
function(name, func) { |
|
4478
|
|
|
// attach the handler to the container element and only listen for real event elements via bubbling |
|
4479
|
|
|
_this.el.on(name, '.fc-event-container > *', function(ev) { |
|
4480
|
|
|
var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents |
|
4481
|
|
|
|
|
4482
|
|
|
// only call the handlers if there is not a drag/resize in progress |
|
4483
|
|
|
if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) { |
|
4484
|
|
|
return func.call(this, seg, ev); // `this` will be the event element |
|
4485
|
|
|
} |
|
4486
|
|
|
}); |
|
4487
|
|
|
} |
|
4488
|
|
|
); |
|
4489
|
|
|
}, |
|
4490
|
|
|
|
|
4491
|
|
|
|
|
4492
|
|
|
// Updates internal state and triggers handlers for when an event element is moused over |
|
4493
|
|
|
triggerSegMouseover: function(seg, ev) { |
|
4494
|
|
|
if (!this.mousedOverSeg) { |
|
4495
|
|
|
this.mousedOverSeg = seg; |
|
4496
|
|
|
this.view.trigger('eventMouseover', seg.el[0], seg.event, ev); |
|
4497
|
|
|
} |
|
4498
|
|
|
}, |
|
4499
|
|
|
|
|
4500
|
|
|
|
|
4501
|
|
|
// Updates internal state and triggers handlers for when an event element is moused out. |
|
4502
|
|
|
// Can be given no arguments, in which case it will mouseout the segment that was previously moused over. |
|
4503
|
|
|
triggerSegMouseout: function(seg, ev) { |
|
4504
|
|
|
ev = ev || {}; // if given no args, make a mock mouse event |
|
4505
|
|
|
|
|
4506
|
|
|
if (this.mousedOverSeg) { |
|
4507
|
|
|
seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment |
|
4508
|
|
|
this.mousedOverSeg = null; |
|
4509
|
|
|
this.view.trigger('eventMouseout', seg.el[0], seg.event, ev); |
|
4510
|
|
|
} |
|
4511
|
|
|
}, |
|
4512
|
|
|
|
|
4513
|
|
|
|
|
4514
|
|
|
/* Dragging |
|
4515
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
4516
|
|
|
|
|
4517
|
|
|
|
|
4518
|
|
|
// Called when the user does a mousedown on an event, which might lead to dragging. |
|
4519
|
|
|
// Generic enough to work with any type of Grid. |
|
4520
|
|
|
segDragMousedown: function(seg, ev) { |
|
4521
|
|
|
var _this = this; |
|
4522
|
|
|
var view = this.view; |
|
4523
|
|
|
var el = seg.el; |
|
4524
|
|
|
var event = seg.event; |
|
4525
|
|
|
var newStart, newEnd; |
|
4526
|
|
|
|
|
4527
|
|
|
// A clone of the original element that will move with the mouse |
|
4528
|
|
|
var mouseFollower = new MouseFollower(seg.el, { |
|
4529
|
|
|
parentEl: view.el, |
|
4530
|
|
|
opacity: view.opt('dragOpacity'), |
|
4531
|
|
|
revertDuration: view.opt('dragRevertDuration'), |
|
4532
|
|
|
zIndex: 2 // one above the .fc-view |
|
4533
|
|
|
}); |
|
4534
|
|
|
|
|
4535
|
|
|
// Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents |
|
4536
|
|
|
// of the view. |
|
4537
|
|
|
var dragListener = new DragListener(view.coordMap, { |
|
4538
|
|
|
distance: 5, |
|
4539
|
|
|
scroll: view.opt('dragScroll'), |
|
4540
|
|
|
listenStart: function(ev) { |
|
4541
|
|
|
mouseFollower.hide(); // don't show until we know this is a real drag |
|
4542
|
|
|
mouseFollower.start(ev); |
|
4543
|
|
|
}, |
|
4544
|
|
|
dragStart: function(ev) { |
|
4545
|
|
|
_this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported |
|
4546
|
|
|
_this.isDraggingSeg = true; |
|
4547
|
|
|
view.hideEvent(event); // hide all event segments. our mouseFollower will take over |
|
4548
|
|
|
view.trigger('eventDragStart', el[0], event, ev, {}); // last argument is jqui dummy |
|
4549
|
|
|
}, |
|
4550
|
|
|
cellOver: function(cell, date) { |
|
4551
|
|
|
var origDate = seg.cellDate || dragListener.origDate; |
|
4552
|
|
|
var res = _this.computeDraggedEventDates(seg, origDate, date); |
|
4553
|
|
|
newStart = res.start; |
|
4554
|
|
|
newEnd = res.end; |
|
4555
|
|
|
|
|
4556
|
|
|
if (view.renderDrag(newStart, newEnd, seg)) { // have the view render a visual indication |
|
4557
|
|
|
mouseFollower.hide(); // if the view is already using a mock event "helper", hide our own |
|
4558
|
|
|
} |
|
4559
|
|
|
else { |
|
4560
|
|
|
mouseFollower.show(); |
|
4561
|
|
|
} |
|
4562
|
|
|
}, |
|
4563
|
|
|
cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells |
|
4564
|
|
|
newStart = null; |
|
4565
|
|
|
view.destroyDrag(); // unrender whatever was done in view.renderDrag |
|
4566
|
|
|
mouseFollower.show(); // show in case we are moving out of all cells |
|
4567
|
|
|
}, |
|
4568
|
|
|
dragStop: function(ev) { |
|
4569
|
|
|
var hasChanged = newStart && !newStart.isSame(event.start); |
|
4570
|
|
|
|
|
4571
|
|
|
// do revert animation if hasn't changed. calls a callback when finished (whether animation or not) |
|
4572
|
|
|
mouseFollower.stop(!hasChanged, function() { |
|
4573
|
|
|
_this.isDraggingSeg = false; |
|
4574
|
|
|
view.destroyDrag(); |
|
4575
|
|
|
view.showEvent(event); |
|
4576
|
|
|
view.trigger('eventDragStop', el[0], event, ev, {}); // last argument is jqui dummy |
|
4577
|
|
|
|
|
4578
|
|
|
if (hasChanged) { |
|
4579
|
|
|
view.eventDrop(el[0], event, newStart, ev); // will rerender all events... |
|
|
|
|
|
|
4580
|
|
|
} |
|
4581
|
|
|
}); |
|
4582
|
|
|
}, |
|
4583
|
|
|
listenStop: function() { |
|
4584
|
|
|
mouseFollower.stop(); // put in listenStop in case there was a mousedown but the drag never started |
|
4585
|
|
|
} |
|
4586
|
|
|
}); |
|
4587
|
|
|
|
|
4588
|
|
|
dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart |
|
4589
|
|
|
}, |
|
4590
|
|
|
|
|
4591
|
|
|
|
|
4592
|
|
|
// Given a segment, the dates where a drag began and ended, calculates the Event Object's new start and end dates |
|
4593
|
|
|
computeDraggedEventDates: function(seg, dragStartDate, dropDate) { |
|
4594
|
|
|
var view = this.view; |
|
4595
|
|
|
var event = seg.event; |
|
4596
|
|
|
var start = event.start; |
|
4597
|
|
|
var end = view.calendar.getEventEnd(event); |
|
4598
|
|
|
var delta; |
|
4599
|
|
|
var newStart; |
|
4600
|
|
|
var newEnd; |
|
4601
|
|
|
|
|
4602
|
|
|
if (dropDate.hasTime() === dragStartDate.hasTime()) { |
|
4603
|
|
|
delta = dayishDiff(dropDate, dragStartDate); |
|
4604
|
|
|
newStart = start.clone().add(delta); |
|
4605
|
|
|
if (event.end === null) { // do we need to compute an end? |
|
4606
|
|
|
newEnd = null; |
|
4607
|
|
|
} |
|
4608
|
|
|
else { |
|
4609
|
|
|
newEnd = end.clone().add(delta); |
|
4610
|
|
|
} |
|
4611
|
|
|
} |
|
4612
|
|
|
else { |
|
4613
|
|
|
// if switching from day <-> timed, start should be reset to the dropped date, and the end cleared |
|
4614
|
|
|
newStart = dropDate; |
|
4615
|
|
|
newEnd = null; // end should be cleared |
|
4616
|
|
|
} |
|
4617
|
|
|
|
|
4618
|
|
|
return { start: newStart, end: newEnd }; |
|
4619
|
|
|
}, |
|
4620
|
|
|
|
|
4621
|
|
|
|
|
4622
|
|
|
/* Resizing |
|
4623
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
4624
|
|
|
|
|
4625
|
|
|
|
|
4626
|
|
|
// Called when the user does a mousedown on an event's resizer, which might lead to resizing. |
|
4627
|
|
|
// Generic enough to work with any type of Grid. |
|
4628
|
|
|
segResizeMousedown: function(seg, ev) { |
|
4629
|
|
|
var _this = this; |
|
4630
|
|
|
var view = this.view; |
|
4631
|
|
|
var el = seg.el; |
|
4632
|
|
|
var event = seg.event; |
|
4633
|
|
|
var start = event.start; |
|
4634
|
|
|
var end = view.calendar.getEventEnd(event); |
|
4635
|
|
|
var newEnd = null; |
|
4636
|
|
|
var dragListener; |
|
4637
|
|
|
|
|
4638
|
|
|
function destroy() { // resets the rendering |
|
4639
|
|
|
_this.destroyResize(); |
|
4640
|
|
|
view.showEvent(event); |
|
4641
|
|
|
} |
|
4642
|
|
|
|
|
4643
|
|
|
// Tracks mouse movement over the *grid's* coordinate map |
|
4644
|
|
|
dragListener = new DragListener(this.coordMap, { |
|
4645
|
|
|
distance: 5, |
|
4646
|
|
|
scroll: view.opt('dragScroll'), |
|
4647
|
|
|
dragStart: function(ev) { |
|
4648
|
|
|
_this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported |
|
4649
|
|
|
_this.isResizingSeg = true; |
|
4650
|
|
|
view.trigger('eventResizeStart', el[0], event, ev, {}); // last argument is jqui dummy |
|
4651
|
|
|
}, |
|
4652
|
|
|
cellOver: function(cell, date) { |
|
4653
|
|
|
// compute the new end. don't allow it to go before the event's start |
|
4654
|
|
|
if (date.isBefore(start)) { // allows comparing ambig to non-ambig |
|
4655
|
|
|
date = start; |
|
4656
|
|
|
} |
|
4657
|
|
|
newEnd = date.clone().add(_this.cellDuration); // make it an exclusive end |
|
4658
|
|
|
|
|
4659
|
|
|
if (newEnd.isSame(end)) { |
|
4660
|
|
|
newEnd = null; |
|
4661
|
|
|
destroy(); |
|
4662
|
|
|
} |
|
4663
|
|
|
else { |
|
4664
|
|
|
_this.renderResize(start, newEnd, seg); |
|
4665
|
|
|
view.hideEvent(event); |
|
4666
|
|
|
} |
|
4667
|
|
|
}, |
|
4668
|
|
|
cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells |
|
4669
|
|
|
newEnd = null; |
|
4670
|
|
|
destroy(); |
|
4671
|
|
|
}, |
|
4672
|
|
|
dragStop: function(ev) { |
|
4673
|
|
|
_this.isResizingSeg = false; |
|
4674
|
|
|
destroy(); |
|
4675
|
|
|
view.trigger('eventResizeStop', el[0], event, ev, {}); // last argument is jqui dummy |
|
4676
|
|
|
|
|
4677
|
|
|
if (newEnd) { |
|
4678
|
|
|
view.eventResize(el[0], event, newEnd, ev); // will rerender all events... |
|
4679
|
|
|
} |
|
4680
|
|
|
} |
|
4681
|
|
|
}); |
|
4682
|
|
|
|
|
4683
|
|
|
dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart |
|
4684
|
|
|
}, |
|
4685
|
|
|
|
|
4686
|
|
|
|
|
4687
|
|
|
/* Rendering Utils |
|
4688
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
4689
|
|
|
|
|
4690
|
|
|
|
|
4691
|
|
|
// Generic utility for generating the HTML classNames for an event segment's element |
|
4692
|
|
|
getSegClasses: function(seg, isDraggable, isResizable) { |
|
4693
|
|
|
var event = seg.event; |
|
4694
|
|
|
var classes = [ |
|
4695
|
|
|
'fc-event', |
|
4696
|
|
|
seg.isStart ? 'fc-start' : 'fc-not-start', |
|
4697
|
|
|
seg.isEnd ? 'fc-end' : 'fc-not-end' |
|
4698
|
|
|
].concat( |
|
4699
|
|
|
event.className, |
|
4700
|
|
|
event.source ? event.source.className : [] |
|
4701
|
|
|
); |
|
4702
|
|
|
|
|
4703
|
|
|
if (isDraggable) { |
|
4704
|
|
|
classes.push('fc-draggable'); |
|
4705
|
|
|
} |
|
4706
|
|
|
if (isResizable) { |
|
4707
|
|
|
classes.push('fc-resizable'); |
|
4708
|
|
|
} |
|
4709
|
|
|
|
|
4710
|
|
|
return classes; |
|
4711
|
|
|
}, |
|
4712
|
|
|
|
|
4713
|
|
|
|
|
4714
|
|
|
// Utility for generating a CSS string with all the event skin-related properties |
|
4715
|
|
|
getEventSkinCss: function(event) { |
|
4716
|
|
|
var view = this.view; |
|
4717
|
|
|
var source = event.source || {}; |
|
4718
|
|
|
var eventColor = event.color; |
|
4719
|
|
|
var sourceColor = source.color; |
|
4720
|
|
|
var optionColor = view.opt('eventColor'); |
|
4721
|
|
|
var backgroundColor = |
|
4722
|
|
|
event.backgroundColor || |
|
4723
|
|
|
eventColor || |
|
4724
|
|
|
source.backgroundColor || |
|
4725
|
|
|
sourceColor || |
|
4726
|
|
|
view.opt('eventBackgroundColor') || |
|
4727
|
|
|
optionColor; |
|
4728
|
|
|
var borderColor = |
|
4729
|
|
|
event.borderColor || |
|
4730
|
|
|
eventColor || |
|
4731
|
|
|
source.borderColor || |
|
4732
|
|
|
sourceColor || |
|
4733
|
|
|
view.opt('eventBorderColor') || |
|
4734
|
|
|
optionColor; |
|
4735
|
|
|
var textColor = |
|
4736
|
|
|
event.textColor || |
|
4737
|
|
|
source.textColor || |
|
4738
|
|
|
view.opt('eventTextColor'); |
|
4739
|
|
|
var statements = []; |
|
4740
|
|
|
if (backgroundColor) { |
|
4741
|
|
|
statements.push('background-color:' + backgroundColor); |
|
4742
|
|
|
} |
|
4743
|
|
|
if (borderColor) { |
|
4744
|
|
|
statements.push('border-color:' + borderColor); |
|
4745
|
|
|
} |
|
4746
|
|
|
if (textColor) { |
|
4747
|
|
|
statements.push('color:' + textColor); |
|
4748
|
|
|
} |
|
4749
|
|
|
return statements.join(';'); |
|
4750
|
|
|
} |
|
4751
|
|
|
|
|
4752
|
|
|
}); |
|
4753
|
|
|
|
|
4754
|
|
|
|
|
4755
|
|
|
/* Event Segment Utilities |
|
4756
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
|
4757
|
|
|
|
|
4758
|
|
|
|
|
4759
|
|
|
// A cmp function for determining which segments should take visual priority |
|
4760
|
|
|
function compareSegs(seg1, seg2) { |
|
4761
|
|
|
return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first |
|
4762
|
|
|
seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first |
|
4763
|
|
|
seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1) |
|
4764
|
|
|
(seg1.event.title || '').localeCompare(seg2.event.title); // tie? alphabetically by title |
|
4765
|
|
|
} |
|
4766
|
|
|
|
|
4767
|
|
|
|
|
4768
|
|
|
;; |
|
4769
|
|
|
|
|
4770
|
|
|
/* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week. |
|
4771
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
|
4772
|
|
|
|
|
4773
|
|
|
function DayGrid(view) { |
|
4774
|
|
|
Grid.call(this, view); // call the super-constructor |
|
4775
|
|
|
} |
|
4776
|
|
|
|
|
4777
|
|
|
|
|
4778
|
|
|
DayGrid.prototype = createObject(Grid.prototype); // declare the super-class |
|
4779
|
|
|
$.extend(DayGrid.prototype, { |
|
4780
|
|
|
|
|
4781
|
|
|
numbersVisible: false, // should render a row for day/week numbers? manually set by the view |
|
4782
|
|
|
cellDuration: moment.duration({ days: 1 }), // required for Grid.event.js. Each cell is always a single day |
|
4783
|
|
|
bottomCoordPadding: 0, // hack for extending the hit area for the last row of the coordinate grid |
|
4784
|
|
|
|
|
4785
|
|
|
rowEls: null, // set of fake row elements |
|
4786
|
|
|
dayEls: null, // set of whole-day elements comprising the row's background |
|
4787
|
|
|
helperEls: null, // set of cell skeleton elements for rendering the mock event "helper" |
|
4788
|
|
|
highlightEls: null, // set of cell skeleton elements for rendering the highlight |
|
4789
|
|
|
|
|
4790
|
|
|
|
|
4791
|
|
|
// Renders the rows and columns into the component's `this.el`, which should already be assigned. |
|
4792
|
|
|
// isRigid determins whether the individual rows should ignore the contents and be a constant height. |
|
4793
|
|
|
// Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient. |
|
4794
|
|
|
render: function(isRigid) { |
|
4795
|
|
|
var view = this.view; |
|
4796
|
|
|
var html = ''; |
|
4797
|
|
|
var row; |
|
4798
|
|
|
|
|
4799
|
|
|
for (row = 0; row < view.rowCnt; row++) { |
|
4800
|
|
|
html += this.dayRowHtml(row, isRigid); |
|
4801
|
|
|
} |
|
4802
|
|
|
this.el.html(html); |
|
4803
|
|
|
|
|
4804
|
|
|
this.rowEls = this.el.find('.fc-row'); |
|
4805
|
|
|
this.dayEls = this.el.find('.fc-day'); |
|
4806
|
|
|
|
|
4807
|
|
|
// run all the day cells through the dayRender callback |
|
4808
|
|
|
this.dayEls.each(function(i, node) { |
|
4809
|
|
|
var date = view.cellToDate(Math.floor(i / view.colCnt), i % view.colCnt); |
|
4810
|
|
|
view.trigger('dayRender', null, date, $(node)); |
|
4811
|
|
|
}); |
|
4812
|
|
|
|
|
4813
|
|
|
Grid.prototype.render.call(this); // call the super-method |
|
4814
|
|
|
}, |
|
4815
|
|
|
|
|
4816
|
|
|
|
|
4817
|
|
|
destroy: function() { |
|
4818
|
|
|
this.destroySegPopover(); |
|
4819
|
|
|
}, |
|
4820
|
|
|
|
|
4821
|
|
|
|
|
4822
|
|
|
// Generates the HTML for a single row. `row` is the row number. |
|
4823
|
|
|
dayRowHtml: function(row, isRigid) { |
|
4824
|
|
|
var view = this.view; |
|
4825
|
|
|
var classes = [ 'fc-row', 'fc-week', view.widgetContentClass ]; |
|
4826
|
|
|
|
|
4827
|
|
|
if (isRigid) { |
|
4828
|
|
|
classes.push('fc-rigid'); |
|
4829
|
|
|
} |
|
4830
|
|
|
|
|
4831
|
|
|
return '' + |
|
4832
|
|
|
'<div class="' + classes.join(' ') + '">' + |
|
4833
|
|
|
'<div class="fc-bg">' + |
|
4834
|
|
|
'<table>' + |
|
4835
|
|
|
this.rowHtml('day', row) + // leverages RowRenderer. calls dayCellHtml() |
|
4836
|
|
|
'</table>' + |
|
4837
|
|
|
'</div>' + |
|
4838
|
|
|
'<div class="fc-content-skeleton">' + |
|
4839
|
|
|
'<table>' + |
|
4840
|
|
|
(this.numbersVisible ? |
|
4841
|
|
|
'<thead>' + |
|
4842
|
|
|
this.rowHtml('number', row) + // leverages RowRenderer. View will define render method |
|
4843
|
|
|
'</thead>' : |
|
4844
|
|
|
'' |
|
4845
|
|
|
) + |
|
4846
|
|
|
'</table>' + |
|
4847
|
|
|
'</div>' + |
|
4848
|
|
|
'</div>'; |
|
4849
|
|
|
}, |
|
4850
|
|
|
|
|
4851
|
|
|
|
|
4852
|
|
|
// Renders the HTML for a whole-day cell. Will eventually end up in the day-row's background. |
|
4853
|
|
|
// We go through a 'day' row type instead of just doing a 'bg' row type so that the View can do custom rendering |
|
4854
|
|
|
// specifically for whole-day rows, whereas a 'bg' might also be used for other purposes (TimeGrid bg for example). |
|
4855
|
|
|
dayCellHtml: function(row, col, date) { |
|
4856
|
|
|
return this.bgCellHtml(row, col, date); |
|
4857
|
|
|
}, |
|
4858
|
|
|
|
|
4859
|
|
|
|
|
4860
|
|
|
/* Coordinates & Cells |
|
4861
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
4862
|
|
|
|
|
4863
|
|
|
|
|
4864
|
|
|
// Populates the empty `rows` and `cols` arrays with coordinates of the cells. For CoordGrid. |
|
4865
|
|
|
buildCoords: function(rows, cols) { |
|
4866
|
|
|
var colCnt = this.view.colCnt; |
|
4867
|
|
|
var e, n, p; |
|
4868
|
|
|
|
|
4869
|
|
|
this.dayEls.slice(0, colCnt).each(function(i, _e) { // iterate the first row of day elements |
|
4870
|
|
|
e = $(_e); |
|
4871
|
|
|
n = e.offset().left; |
|
4872
|
|
|
if (i) { |
|
4873
|
|
|
p[1] = n; |
|
4874
|
|
|
} |
|
4875
|
|
|
p = [ n ]; |
|
4876
|
|
|
cols[i] = p; |
|
4877
|
|
|
}); |
|
4878
|
|
|
p[1] = n + e.outerWidth(); |
|
4879
|
|
|
|
|
4880
|
|
|
this.rowEls.each(function(i, _e) { |
|
4881
|
|
|
e = $(_e); |
|
4882
|
|
|
n = e.offset().top; |
|
4883
|
|
|
if (i) { |
|
4884
|
|
|
p[1] = n; |
|
4885
|
|
|
} |
|
4886
|
|
|
p = [ n ]; |
|
4887
|
|
|
rows[i] = p; |
|
4888
|
|
|
}); |
|
4889
|
|
|
p[1] = n + e.outerHeight() + this.bottomCoordPadding; // hack to extend hit area of last row |
|
4890
|
|
|
}, |
|
4891
|
|
|
|
|
4892
|
|
|
|
|
4893
|
|
|
// Converts a cell to a date |
|
4894
|
|
|
getCellDate: function(cell) { |
|
4895
|
|
|
return this.view.cellToDate(cell); // leverages the View's cell system |
|
4896
|
|
|
}, |
|
4897
|
|
|
|
|
4898
|
|
|
|
|
4899
|
|
|
// Gets the whole-day element associated with the cell |
|
4900
|
|
|
getCellDayEl: function(cell) { |
|
4901
|
|
|
return this.dayEls.eq(cell.row * this.view.colCnt + cell.col); |
|
4902
|
|
|
}, |
|
4903
|
|
|
|
|
4904
|
|
|
|
|
4905
|
|
|
// Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects |
|
4906
|
|
|
rangeToSegs: function(start, end) { |
|
4907
|
|
|
return this.view.rangeToSegments(start, end); // leverages the View's cell system |
|
4908
|
|
|
}, |
|
4909
|
|
|
|
|
4910
|
|
|
|
|
4911
|
|
|
/* Event Drag Visualization |
|
4912
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
4913
|
|
|
|
|
4914
|
|
|
|
|
4915
|
|
|
// Renders a visual indication of an event hovering over the given date(s). |
|
4916
|
|
|
// `end` can be null, as well as `seg`. See View's documentation on renderDrag for more info. |
|
4917
|
|
|
// A returned value of `true` signals that a mock "helper" event has been rendered. |
|
4918
|
|
|
renderDrag: function(start, end, seg) { |
|
4919
|
|
|
var opacity; |
|
4920
|
|
|
|
|
4921
|
|
|
// always render a highlight underneath |
|
4922
|
|
|
this.renderHighlight( |
|
4923
|
|
|
start, |
|
4924
|
|
|
end || this.view.calendar.getDefaultEventEnd(true, start) |
|
4925
|
|
|
); |
|
4926
|
|
|
|
|
4927
|
|
|
// if a segment from the same calendar but another component is being dragged, render a helper event |
|
4928
|
|
|
if (seg && !seg.el.closest(this.el).length) { |
|
4929
|
|
|
|
|
4930
|
|
|
this.renderRangeHelper(start, end, seg); |
|
4931
|
|
|
|
|
4932
|
|
|
opacity = this.view.opt('dragOpacity'); |
|
4933
|
|
|
if (opacity !== undefined) { |
|
4934
|
|
|
this.helperEls.css('opacity', opacity); |
|
4935
|
|
|
} |
|
4936
|
|
|
|
|
4937
|
|
|
return true; // a helper has been rendered |
|
4938
|
|
|
} |
|
4939
|
|
|
}, |
|
4940
|
|
|
|
|
4941
|
|
|
|
|
4942
|
|
|
// Unrenders any visual indication of a hovering event |
|
4943
|
|
|
destroyDrag: function() { |
|
4944
|
|
|
this.destroyHighlight(); |
|
4945
|
|
|
this.destroyHelper(); |
|
4946
|
|
|
}, |
|
4947
|
|
|
|
|
4948
|
|
|
|
|
4949
|
|
|
/* Event Resize Visualization |
|
4950
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
4951
|
|
|
|
|
4952
|
|
|
|
|
4953
|
|
|
// Renders a visual indication of an event being resized |
|
4954
|
|
|
renderResize: function(start, end, seg) { |
|
4955
|
|
|
this.renderHighlight(start, end); |
|
4956
|
|
|
this.renderRangeHelper(start, end, seg); |
|
4957
|
|
|
}, |
|
4958
|
|
|
|
|
4959
|
|
|
|
|
4960
|
|
|
// Unrenders a visual indication of an event being resized |
|
4961
|
|
|
destroyResize: function() { |
|
4962
|
|
|
this.destroyHighlight(); |
|
4963
|
|
|
this.destroyHelper(); |
|
4964
|
|
|
}, |
|
4965
|
|
|
|
|
4966
|
|
|
|
|
4967
|
|
|
/* Event Helper |
|
4968
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
4969
|
|
|
|
|
4970
|
|
|
|
|
4971
|
|
|
// Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null. |
|
4972
|
|
|
renderHelper: function(event, sourceSeg) { |
|
4973
|
|
|
var helperNodes = []; |
|
4974
|
|
|
var rowStructs = this.renderEventRows([ event ]); |
|
4975
|
|
|
|
|
4976
|
|
|
// inject each new event skeleton into each associated row |
|
4977
|
|
|
this.rowEls.each(function(row, rowNode) { |
|
4978
|
|
|
var rowEl = $(rowNode); // the .fc-row |
|
4979
|
|
|
var skeletonEl = $('<div class="fc-helper-skeleton"><table/></div>'); // will be absolutely positioned |
|
4980
|
|
|
var skeletonTop; |
|
4981
|
|
|
|
|
4982
|
|
|
// If there is an original segment, match the top position. Otherwise, put it at the row's top level |
|
4983
|
|
|
if (sourceSeg && sourceSeg.row === row) { |
|
4984
|
|
|
skeletonTop = sourceSeg.el.position().top; |
|
4985
|
|
|
} |
|
4986
|
|
|
else { |
|
4987
|
|
|
skeletonTop = rowEl.find('.fc-content-skeleton tbody').position().top; |
|
4988
|
|
|
} |
|
4989
|
|
|
|
|
4990
|
|
|
skeletonEl.css('top', skeletonTop) |
|
4991
|
|
|
.find('table') |
|
4992
|
|
|
.append(rowStructs[row].tbodyEl); |
|
4993
|
|
|
|
|
4994
|
|
|
rowEl.append(skeletonEl); |
|
4995
|
|
|
helperNodes.push(skeletonEl[0]); |
|
4996
|
|
|
}); |
|
4997
|
|
|
|
|
4998
|
|
|
this.helperEls = $(helperNodes); // array -> jQuery set |
|
4999
|
|
|
}, |
|
5000
|
|
|
|
|
5001
|
|
|
|
|
5002
|
|
|
// Unrenders any visual indication of a mock helper event |
|
5003
|
|
|
destroyHelper: function() { |
|
5004
|
|
|
if (this.helperEls) { |
|
5005
|
|
|
this.helperEls.remove(); |
|
5006
|
|
|
this.helperEls = null; |
|
5007
|
|
|
} |
|
5008
|
|
|
}, |
|
5009
|
|
|
|
|
5010
|
|
|
|
|
5011
|
|
|
/* Highlighting |
|
5012
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
5013
|
|
|
|
|
5014
|
|
|
|
|
5015
|
|
|
// Renders an emphasis on the given date range. `start` is an inclusive, `end` is exclusive. |
|
5016
|
|
|
renderHighlight: function(start, end) { |
|
5017
|
|
|
var segs = this.rangeToSegs(start, end); |
|
5018
|
|
|
var highlightNodes = []; |
|
5019
|
|
|
var i, seg; |
|
5020
|
|
|
var el; |
|
5021
|
|
|
|
|
5022
|
|
|
// build an event skeleton for each row that needs it |
|
5023
|
|
|
for (i = 0; i < segs.length; i++) { |
|
5024
|
|
|
seg = segs[i]; |
|
5025
|
|
|
el = $( |
|
5026
|
|
|
this.highlightSkeletonHtml(seg.leftCol, seg.rightCol + 1) // make end exclusive |
|
5027
|
|
|
); |
|
5028
|
|
|
el.appendTo(this.rowEls[seg.row]); |
|
5029
|
|
|
highlightNodes.push(el[0]); |
|
5030
|
|
|
} |
|
5031
|
|
|
|
|
5032
|
|
|
this.highlightEls = $(highlightNodes); // array -> jQuery set |
|
5033
|
|
|
}, |
|
5034
|
|
|
|
|
5035
|
|
|
|
|
5036
|
|
|
// Unrenders any visual emphasis on a date range |
|
5037
|
|
|
destroyHighlight: function() { |
|
5038
|
|
|
if (this.highlightEls) { |
|
5039
|
|
|
this.highlightEls.remove(); |
|
5040
|
|
|
this.highlightEls = null; |
|
5041
|
|
|
} |
|
5042
|
|
|
}, |
|
5043
|
|
|
|
|
5044
|
|
|
|
|
5045
|
|
|
// Generates the HTML used to build a single-row "highlight skeleton", a table that frames highlight cells |
|
5046
|
|
|
highlightSkeletonHtml: function(startCol, endCol) { |
|
5047
|
|
|
var colCnt = this.view.colCnt; |
|
5048
|
|
|
var cellHtml = ''; |
|
5049
|
|
|
|
|
5050
|
|
|
if (startCol > 0) { |
|
5051
|
|
|
cellHtml += '<td colspan="' + startCol + '"/>'; |
|
5052
|
|
|
} |
|
5053
|
|
|
if (endCol > startCol) { |
|
5054
|
|
|
cellHtml += '<td colspan="' + (endCol - startCol) + '" class="fc-highlight" />'; |
|
5055
|
|
|
} |
|
5056
|
|
|
if (colCnt > endCol) { |
|
5057
|
|
|
cellHtml += '<td colspan="' + (colCnt - endCol) + '"/>'; |
|
5058
|
|
|
} |
|
5059
|
|
|
|
|
5060
|
|
|
cellHtml = this.bookendCells(cellHtml, 'highlight'); |
|
5061
|
|
|
|
|
5062
|
|
|
return '' + |
|
5063
|
|
|
'<div class="fc-highlight-skeleton">' + |
|
5064
|
|
|
'<table>' + |
|
5065
|
|
|
'<tr>' + |
|
5066
|
|
|
cellHtml + |
|
5067
|
|
|
'</tr>' + |
|
5068
|
|
|
'</table>' + |
|
5069
|
|
|
'</div>'; |
|
5070
|
|
|
} |
|
5071
|
|
|
|
|
5072
|
|
|
}); |
|
5073
|
|
|
|
|
5074
|
|
|
;; |
|
5075
|
|
|
|
|
5076
|
|
|
/* Event-rendering methods for the DayGrid class |
|
5077
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
|
5078
|
|
|
|
|
5079
|
|
|
$.extend(DayGrid.prototype, { |
|
5080
|
|
|
|
|
5081
|
|
|
segs: null, |
|
5082
|
|
|
rowStructs: null, // an array of objects, each holding information about a row's event-rendering |
|
5083
|
|
|
|
|
5084
|
|
|
|
|
5085
|
|
|
// Render the given events onto the Grid and return the rendered segments |
|
5086
|
|
|
renderEvents: function(events) { |
|
5087
|
|
|
var rowStructs = this.rowStructs = this.renderEventRows(events); |
|
5088
|
|
|
var segs = []; |
|
5089
|
|
|
|
|
5090
|
|
|
// append to each row's content skeleton |
|
5091
|
|
|
this.rowEls.each(function(i, rowNode) { |
|
5092
|
|
|
$(rowNode).find('.fc-content-skeleton > table').append( |
|
5093
|
|
|
rowStructs[i].tbodyEl |
|
5094
|
|
|
); |
|
5095
|
|
|
segs.push.apply(segs, rowStructs[i].segs); |
|
5096
|
|
|
}); |
|
5097
|
|
|
|
|
5098
|
|
|
this.segs = segs; |
|
5099
|
|
|
}, |
|
5100
|
|
|
|
|
5101
|
|
|
|
|
5102
|
|
|
// Retrieves all segment objects that have been rendered |
|
5103
|
|
|
getSegs: function() { |
|
5104
|
|
|
return (this.segs || []).concat( |
|
5105
|
|
|
this.popoverSegs || [] // segs rendered in the "more" events popover |
|
5106
|
|
|
); |
|
5107
|
|
|
}, |
|
5108
|
|
|
|
|
5109
|
|
|
|
|
5110
|
|
|
// Removes all rendered event elements |
|
5111
|
|
|
destroyEvents: function() { |
|
5112
|
|
|
var rowStructs; |
|
5113
|
|
|
var rowStruct; |
|
5114
|
|
|
|
|
5115
|
|
|
Grid.prototype.destroyEvents.call(this); // call the super-method |
|
5116
|
|
|
|
|
5117
|
|
|
rowStructs = this.rowStructs || []; |
|
5118
|
|
|
while ((rowStruct = rowStructs.pop())) { |
|
5119
|
|
|
rowStruct.tbodyEl.remove(); |
|
5120
|
|
|
} |
|
5121
|
|
|
|
|
5122
|
|
|
this.segs = null; |
|
5123
|
|
|
this.destroySegPopover(); // removes the "more.." events popover |
|
5124
|
|
|
}, |
|
5125
|
|
|
|
|
5126
|
|
|
|
|
5127
|
|
|
// Uses the given events array to generate <tbody> elements that should be appended to each row's content skeleton. |
|
5128
|
|
|
// Returns an array of rowStruct objects (see the bottom of `renderEventRow`). |
|
5129
|
|
|
renderEventRows: function(events) { |
|
5130
|
|
|
var segs = this.eventsToSegs(events); |
|
5131
|
|
|
var rowStructs = []; |
|
5132
|
|
|
var segRows; |
|
5133
|
|
|
var row; |
|
5134
|
|
|
|
|
5135
|
|
|
segs = this.renderSegs(segs); // returns a new array with only visible segments |
|
5136
|
|
|
segRows = this.groupSegRows(segs); // group into nested arrays |
|
5137
|
|
|
|
|
5138
|
|
|
// iterate each row of segment groupings |
|
5139
|
|
|
for (row = 0; row < segRows.length; row++) { |
|
5140
|
|
|
rowStructs.push( |
|
5141
|
|
|
this.renderEventRow(row, segRows[row]) |
|
5142
|
|
|
); |
|
5143
|
|
|
} |
|
5144
|
|
|
|
|
5145
|
|
|
return rowStructs; |
|
5146
|
|
|
}, |
|
5147
|
|
|
|
|
5148
|
|
|
|
|
5149
|
|
|
// Builds the HTML to be used for the default element for an individual segment |
|
5150
|
|
|
renderSegHtml: function(seg, disableResizing) { |
|
5151
|
|
|
var view = this.view; |
|
5152
|
|
|
var isRTL = view.opt('isRTL'); |
|
5153
|
|
|
var event = seg.event; |
|
5154
|
|
|
var isDraggable = view.isEventDraggable(event); |
|
5155
|
|
|
var isResizable = !disableResizing && event.allDay && seg.isEnd && view.isEventResizable(event); |
|
5156
|
|
|
var classes = this.getSegClasses(seg, isDraggable, isResizable); |
|
5157
|
|
|
var skinCss = this.getEventSkinCss(event); |
|
5158
|
|
|
var timeHtml = ''; |
|
5159
|
|
|
var titleHtml; |
|
5160
|
|
|
|
|
5161
|
|
|
classes.unshift('fc-day-grid-event'); |
|
5162
|
|
|
|
|
5163
|
|
|
// Only display a timed events time if it is the starting segment |
|
5164
|
|
|
if (!event.allDay && seg.isStart) { |
|
5165
|
|
|
timeHtml = '<span class="fc-time">' + htmlEscape(view.getEventTimeText(event)) + '</span>'; |
|
5166
|
|
|
} |
|
5167
|
|
|
|
|
5168
|
|
|
titleHtml = |
|
5169
|
|
|
'<span class="fc-title">' + |
|
5170
|
|
|
(htmlEscape(event.title || '') || ' ') + // we always want one line of height |
|
5171
|
|
|
'</span>'; |
|
5172
|
|
|
|
|
5173
|
|
|
return '<a class="' + classes.join(' ') + '"' + |
|
5174
|
|
|
(event.url ? |
|
5175
|
|
|
' href="' + htmlEscape(event.url) + '"' : |
|
5176
|
|
|
'' |
|
5177
|
|
|
) + |
|
5178
|
|
|
(skinCss ? |
|
5179
|
|
|
' style="' + skinCss + '"' : |
|
5180
|
|
|
'' |
|
5181
|
|
|
) + |
|
5182
|
|
|
'>' + |
|
5183
|
|
|
'<div class="fc-content">' + |
|
5184
|
|
|
(isRTL ? |
|
5185
|
|
|
titleHtml + ' ' + timeHtml : // put a natural space in between |
|
5186
|
|
|
timeHtml + ' ' + titleHtml // |
|
5187
|
|
|
) + |
|
5188
|
|
|
'</div>' + |
|
5189
|
|
|
(isResizable ? |
|
5190
|
|
|
'<div class="fc-resizer"/>' : |
|
5191
|
|
|
'' |
|
5192
|
|
|
) + |
|
5193
|
|
|
'</a>'; |
|
5194
|
|
|
}, |
|
5195
|
|
|
|
|
5196
|
|
|
|
|
5197
|
|
|
// Given a row # and an array of segments all in the same row, render a <tbody> element, a skeleton that contains |
|
5198
|
|
|
// the segments. Returns object with a bunch of internal data about how the render was calculated. |
|
5199
|
|
|
renderEventRow: function(row, rowSegs) { |
|
5200
|
|
|
var view = this.view; |
|
5201
|
|
|
var colCnt = view.colCnt; |
|
5202
|
|
|
var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels |
|
5203
|
|
|
var levelCnt = Math.max(1, segLevels.length); // ensure at least one level |
|
5204
|
|
|
var tbody = $('<tbody/>'); |
|
5205
|
|
|
var segMatrix = []; // lookup for which segments are rendered into which level+col cells |
|
5206
|
|
|
var cellMatrix = []; // lookup for all <td> elements of the level+col matrix |
|
5207
|
|
|
var loneCellMatrix = []; // lookup for <td> elements that only take up a single column |
|
5208
|
|
|
var i, levelSegs; |
|
5209
|
|
|
var col; |
|
5210
|
|
|
var tr; |
|
5211
|
|
|
var j, seg; |
|
5212
|
|
|
var td; |
|
5213
|
|
|
|
|
5214
|
|
|
// populates empty cells from the current column (`col`) to `endCol` |
|
5215
|
|
|
function emptyCellsUntil(endCol) { |
|
5216
|
|
|
while (col < endCol) { |
|
|
|
|
|
|
5217
|
|
|
// try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell |
|
5218
|
|
|
td = (loneCellMatrix[i - 1] || [])[col]; |
|
5219
|
|
|
if (td) { |
|
|
|
|
|
|
5220
|
|
|
td.attr( |
|
5221
|
|
|
'rowspan', |
|
5222
|
|
|
parseInt(td.attr('rowspan') || 1, 10) + 1 |
|
5223
|
|
|
); |
|
5224
|
|
|
} |
|
5225
|
|
|
else { |
|
5226
|
|
|
td = $('<td/>'); |
|
5227
|
|
|
tr.append(td); |
|
5228
|
|
|
} |
|
5229
|
|
|
cellMatrix[i][col] = td; |
|
5230
|
|
|
loneCellMatrix[i][col] = td; |
|
5231
|
|
|
col++; |
|
5232
|
|
|
} |
|
5233
|
|
|
} |
|
5234
|
|
|
|
|
5235
|
|
|
for (i = 0; i < levelCnt; i++) { // iterate through all levels |
|
5236
|
|
|
levelSegs = segLevels[i]; |
|
5237
|
|
|
col = 0; |
|
5238
|
|
|
tr = $('<tr/>'); |
|
5239
|
|
|
|
|
5240
|
|
|
segMatrix.push([]); |
|
5241
|
|
|
cellMatrix.push([]); |
|
5242
|
|
|
loneCellMatrix.push([]); |
|
5243
|
|
|
|
|
5244
|
|
|
// levelCnt might be 1 even though there are no actual levels. protect against this. |
|
5245
|
|
|
// this single empty row is useful for styling. |
|
5246
|
|
|
if (levelSegs) { |
|
5247
|
|
|
for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level |
|
5248
|
|
|
seg = levelSegs[j]; |
|
5249
|
|
|
|
|
5250
|
|
|
emptyCellsUntil(seg.leftCol); |
|
5251
|
|
|
|
|
5252
|
|
|
// create a container that occupies or more columns. append the event element. |
|
5253
|
|
|
td = $('<td class="fc-event-container"/>').append(seg.el); |
|
5254
|
|
|
if (seg.leftCol != seg.rightCol) { |
|
5255
|
|
|
td.attr('colspan', seg.rightCol - seg.leftCol + 1); |
|
5256
|
|
|
} |
|
5257
|
|
|
else { // a single-column segment |
|
5258
|
|
|
loneCellMatrix[i][col] = td; |
|
5259
|
|
|
} |
|
5260
|
|
|
|
|
5261
|
|
|
while (col <= seg.rightCol) { |
|
5262
|
|
|
cellMatrix[i][col] = td; |
|
5263
|
|
|
segMatrix[i][col] = seg; |
|
5264
|
|
|
col++; |
|
5265
|
|
|
} |
|
5266
|
|
|
|
|
5267
|
|
|
tr.append(td); |
|
5268
|
|
|
} |
|
5269
|
|
|
} |
|
5270
|
|
|
|
|
5271
|
|
|
emptyCellsUntil(colCnt); // finish off the row |
|
5272
|
|
|
this.bookendCells(tr, 'eventSkeleton'); |
|
5273
|
|
|
tbody.append(tr); |
|
5274
|
|
|
} |
|
5275
|
|
|
|
|
5276
|
|
|
return { // a "rowStruct" |
|
5277
|
|
|
row: row, // the row number |
|
5278
|
|
|
tbodyEl: tbody, |
|
5279
|
|
|
cellMatrix: cellMatrix, |
|
5280
|
|
|
segMatrix: segMatrix, |
|
5281
|
|
|
segLevels: segLevels, |
|
5282
|
|
|
segs: rowSegs |
|
5283
|
|
|
}; |
|
5284
|
|
|
}, |
|
5285
|
|
|
|
|
5286
|
|
|
|
|
5287
|
|
|
// Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels. |
|
5288
|
|
|
buildSegLevels: function(segs) { |
|
5289
|
|
|
var levels = []; |
|
5290
|
|
|
var i, seg; |
|
5291
|
|
|
var j; |
|
5292
|
|
|
|
|
5293
|
|
|
// Give preference to elements with certain criteria, so they have |
|
5294
|
|
|
// a chance to be closer to the top. |
|
5295
|
|
|
segs.sort(compareSegs); |
|
5296
|
|
|
|
|
5297
|
|
|
for (i = 0; i < segs.length; i++) { |
|
5298
|
|
|
seg = segs[i]; |
|
5299
|
|
|
|
|
5300
|
|
|
// loop through levels, starting with the topmost, until the segment doesn't collide with other segments |
|
5301
|
|
|
for (j = 0; j < levels.length; j++) { |
|
5302
|
|
|
if (!isDaySegCollision(seg, levels[j])) { |
|
5303
|
|
|
break; |
|
5304
|
|
|
} |
|
5305
|
|
|
} |
|
5306
|
|
|
// `j` now holds the desired subrow index |
|
5307
|
|
|
seg.level = j; |
|
5308
|
|
|
|
|
5309
|
|
|
// create new level array if needed and append segment |
|
5310
|
|
|
(levels[j] || (levels[j] = [])).push(seg); |
|
5311
|
|
|
} |
|
5312
|
|
|
|
|
5313
|
|
|
// order segments left-to-right. very important if calendar is RTL |
|
5314
|
|
|
for (j = 0; j < levels.length; j++) { |
|
5315
|
|
|
levels[j].sort(compareDaySegCols); |
|
5316
|
|
|
} |
|
5317
|
|
|
|
|
5318
|
|
|
return levels; |
|
5319
|
|
|
}, |
|
5320
|
|
|
|
|
5321
|
|
|
|
|
5322
|
|
|
// Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row |
|
5323
|
|
|
groupSegRows: function(segs) { |
|
5324
|
|
|
var view = this.view; |
|
5325
|
|
|
var segRows = []; |
|
5326
|
|
|
var i; |
|
5327
|
|
|
|
|
5328
|
|
|
for (i = 0; i < view.rowCnt; i++) { |
|
5329
|
|
|
segRows.push([]); |
|
5330
|
|
|
} |
|
5331
|
|
|
|
|
5332
|
|
|
for (i = 0; i < segs.length; i++) { |
|
5333
|
|
|
segRows[segs[i].row].push(segs[i]); |
|
5334
|
|
|
} |
|
5335
|
|
|
|
|
5336
|
|
|
return segRows; |
|
5337
|
|
|
} |
|
5338
|
|
|
|
|
5339
|
|
|
}); |
|
5340
|
|
|
|
|
5341
|
|
|
|
|
5342
|
|
|
// Computes whether two segments' columns collide. They are assumed to be in the same row. |
|
5343
|
|
|
function isDaySegCollision(seg, otherSegs) { |
|
5344
|
|
|
var i, otherSeg; |
|
5345
|
|
|
|
|
5346
|
|
|
for (i = 0; i < otherSegs.length; i++) { |
|
5347
|
|
|
otherSeg = otherSegs[i]; |
|
5348
|
|
|
|
|
5349
|
|
|
if ( |
|
5350
|
|
|
otherSeg.leftCol <= seg.rightCol && |
|
5351
|
|
|
otherSeg.rightCol >= seg.leftCol |
|
5352
|
|
|
) { |
|
5353
|
|
|
return true; |
|
5354
|
|
|
} |
|
5355
|
|
|
} |
|
5356
|
|
|
|
|
5357
|
|
|
return false; |
|
5358
|
|
|
} |
|
5359
|
|
|
|
|
5360
|
|
|
|
|
5361
|
|
|
// A cmp function for determining the leftmost event |
|
5362
|
|
|
function compareDaySegCols(a, b) { |
|
5363
|
|
|
return a.leftCol - b.leftCol; |
|
5364
|
|
|
} |
|
5365
|
|
|
|
|
5366
|
|
|
;; |
|
5367
|
|
|
|
|
5368
|
|
|
/* Methods relate to limiting the number events for a given day on a DayGrid |
|
5369
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
|
5370
|
|
|
|
|
5371
|
|
|
$.extend(DayGrid.prototype, { |
|
5372
|
|
|
|
|
5373
|
|
|
|
|
5374
|
|
|
segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible |
|
5375
|
|
|
popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible |
|
5376
|
|
|
|
|
5377
|
|
|
|
|
5378
|
|
|
destroySegPopover: function() { |
|
5379
|
|
|
if (this.segPopover) { |
|
5380
|
|
|
this.segPopover.hide(); // will trigger destruction of `segPopover` and `popoverSegs` |
|
5381
|
|
|
} |
|
5382
|
|
|
}, |
|
5383
|
|
|
|
|
5384
|
|
|
|
|
5385
|
|
|
// Limits the number of "levels" (vertically stacking layers of events) for each row of the grid. |
|
5386
|
|
|
// `levelLimit` can be false (don't limit), a number, or true (should be computed). |
|
5387
|
|
|
limitRows: function(levelLimit) { |
|
5388
|
|
|
var rowStructs = this.rowStructs || []; |
|
5389
|
|
|
var row; // row # |
|
5390
|
|
|
var rowLevelLimit; |
|
5391
|
|
|
|
|
5392
|
|
|
for (row = 0; row < rowStructs.length; row++) { |
|
5393
|
|
|
this.unlimitRow(row); |
|
5394
|
|
|
|
|
5395
|
|
|
if (!levelLimit) { |
|
5396
|
|
|
rowLevelLimit = false; |
|
5397
|
|
|
} |
|
5398
|
|
|
else if (typeof levelLimit === 'number') { |
|
5399
|
|
|
rowLevelLimit = levelLimit; |
|
5400
|
|
|
} |
|
5401
|
|
|
else { |
|
5402
|
|
|
rowLevelLimit = this.computeRowLevelLimit(row); |
|
5403
|
|
|
} |
|
5404
|
|
|
|
|
5405
|
|
|
if (rowLevelLimit !== false) { |
|
5406
|
|
|
this.limitRow(row, rowLevelLimit); |
|
5407
|
|
|
} |
|
5408
|
|
|
} |
|
5409
|
|
|
}, |
|
5410
|
|
|
|
|
5411
|
|
|
|
|
5412
|
|
|
// Computes the number of levels a row will accomodate without going outside its bounds. |
|
5413
|
|
|
// Assumes the row is "rigid" (maintains a constant height regardless of what is inside). |
|
5414
|
|
|
// `row` is the row number. |
|
5415
|
|
|
computeRowLevelLimit: function(row) { |
|
5416
|
|
|
var rowEl = this.rowEls.eq(row); // the containing "fake" row div |
|
5417
|
|
|
var rowHeight = rowEl.height(); // TODO: cache somehow? |
|
5418
|
|
|
var trEls = this.rowStructs[row].tbodyEl.children(); |
|
5419
|
|
|
var i, trEl; |
|
5420
|
|
|
|
|
5421
|
|
|
// Reveal one level <tr> at a time and stop when we find one out of bounds |
|
5422
|
|
|
for (i = 0; i < trEls.length; i++) { |
|
5423
|
|
|
trEl = trEls.eq(i).removeClass('fc-limited'); // get and reveal |
|
5424
|
|
|
if (trEl.position().top + trEl.outerHeight() > rowHeight) { |
|
5425
|
|
|
return i; |
|
5426
|
|
|
} |
|
5427
|
|
|
} |
|
5428
|
|
|
|
|
5429
|
|
|
return false; // should not limit at all |
|
5430
|
|
|
}, |
|
5431
|
|
|
|
|
5432
|
|
|
|
|
5433
|
|
|
// Limits the given grid row to the maximum number of levels and injects "more" links if necessary. |
|
5434
|
|
|
// `row` is the row number. |
|
5435
|
|
|
// `levelLimit` is a number for the maximum (inclusive) number of levels allowed. |
|
5436
|
|
|
limitRow: function(row, levelLimit) { |
|
5437
|
|
|
var _this = this; |
|
5438
|
|
|
var view = this.view; |
|
5439
|
|
|
var rowStruct = this.rowStructs[row]; |
|
5440
|
|
|
var moreNodes = []; // array of "more" <a> links and <td> DOM nodes |
|
5441
|
|
|
var col = 0; // col # |
|
5442
|
|
|
var cell; |
|
5443
|
|
|
var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right |
|
5444
|
|
|
var cellMatrix; // a matrix (by level, then column) of all <td> jQuery elements in the row |
|
5445
|
|
|
var limitedNodes; // array of temporarily hidden level <tr> and segment <td> DOM nodes |
|
5446
|
|
|
var i, seg; |
|
5447
|
|
|
var segsBelow; // array of segment objects below `seg` in the current `col` |
|
5448
|
|
|
var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies |
|
5449
|
|
|
var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column) |
|
5450
|
|
|
var td, rowspan; |
|
5451
|
|
|
var segMoreNodes; // array of "more" <td> cells that will stand-in for the current seg's cell |
|
5452
|
|
|
var j; |
|
5453
|
|
|
var moreTd, moreWrap, moreLink; |
|
5454
|
|
|
|
|
5455
|
|
|
// Iterates through empty level cells and places "more" links inside if need be |
|
5456
|
|
|
function emptyCellsUntil(endCol) { // goes from current `col` to `endCol` |
|
5457
|
|
|
while (col < endCol) { |
|
|
|
|
|
|
5458
|
|
|
cell = { row: row, col: col }; |
|
5459
|
|
|
segsBelow = _this.getCellSegs(cell, levelLimit); |
|
|
|
|
|
|
5460
|
|
|
if (segsBelow.length) { |
|
|
|
|
|
|
5461
|
|
|
td = cellMatrix[levelLimit - 1][col]; |
|
5462
|
|
|
moreLink = _this.renderMoreLink(cell, segsBelow); |
|
5463
|
|
|
moreWrap = $('<div/>').append(moreLink); |
|
|
|
|
|
|
5464
|
|
|
td.append(moreWrap); |
|
|
|
|
|
|
5465
|
|
|
moreNodes.push(moreWrap[0]); |
|
5466
|
|
|
} |
|
5467
|
|
|
col++; |
|
5468
|
|
|
} |
|
5469
|
|
|
} |
|
5470
|
|
|
|
|
5471
|
|
|
if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit? |
|
5472
|
|
|
levelSegs = rowStruct.segLevels[levelLimit - 1]; |
|
5473
|
|
|
cellMatrix = rowStruct.cellMatrix; |
|
5474
|
|
|
|
|
5475
|
|
|
limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level <tr> elements past the limit |
|
5476
|
|
|
.addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array |
|
5477
|
|
|
|
|
5478
|
|
|
// iterate though segments in the last allowable level |
|
5479
|
|
|
for (i = 0; i < levelSegs.length; i++) { |
|
5480
|
|
|
seg = levelSegs[i]; |
|
5481
|
|
|
emptyCellsUntil(seg.leftCol); // process empty cells before the segment |
|
5482
|
|
|
|
|
5483
|
|
|
// determine *all* segments below `seg` that occupy the same columns |
|
5484
|
|
|
colSegsBelow = []; |
|
5485
|
|
|
totalSegsBelow = 0; |
|
5486
|
|
|
while (col <= seg.rightCol) { |
|
5487
|
|
|
cell = { row: row, col: col }; |
|
5488
|
|
|
segsBelow = this.getCellSegs(cell, levelLimit); |
|
5489
|
|
|
colSegsBelow.push(segsBelow); |
|
5490
|
|
|
totalSegsBelow += segsBelow.length; |
|
5491
|
|
|
col++; |
|
5492
|
|
|
} |
|
5493
|
|
|
|
|
5494
|
|
|
if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links? |
|
5495
|
|
|
td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell |
|
5496
|
|
|
rowspan = td.attr('rowspan') || 1; |
|
5497
|
|
|
segMoreNodes = []; |
|
5498
|
|
|
|
|
5499
|
|
|
// make a replacement <td> for each column the segment occupies. will be one for each colspan |
|
5500
|
|
|
for (j = 0; j < colSegsBelow.length; j++) { |
|
5501
|
|
|
moreTd = $('<td class="fc-more-cell"/>').attr('rowspan', rowspan); |
|
5502
|
|
|
segsBelow = colSegsBelow[j]; |
|
5503
|
|
|
cell = { row: row, col: seg.leftCol + j }; |
|
5504
|
|
|
moreLink = this.renderMoreLink(cell, [ seg ].concat(segsBelow)); // count seg as hidden too |
|
5505
|
|
|
moreWrap = $('<div/>').append(moreLink); |
|
5506
|
|
|
moreTd.append(moreWrap); |
|
5507
|
|
|
segMoreNodes.push(moreTd[0]); |
|
5508
|
|
|
moreNodes.push(moreTd[0]); |
|
5509
|
|
|
} |
|
5510
|
|
|
|
|
5511
|
|
|
td.addClass('fc-limited').after($(segMoreNodes)); // hide original <td> and inject replacements |
|
5512
|
|
|
limitedNodes.push(td[0]); |
|
5513
|
|
|
} |
|
5514
|
|
|
} |
|
5515
|
|
|
|
|
5516
|
|
|
emptyCellsUntil(view.colCnt); // finish off the level |
|
5517
|
|
|
rowStruct.moreEls = $(moreNodes); // for easy undoing later |
|
5518
|
|
|
rowStruct.limitedEls = $(limitedNodes); // for easy undoing later |
|
5519
|
|
|
} |
|
5520
|
|
|
}, |
|
5521
|
|
|
|
|
5522
|
|
|
|
|
5523
|
|
|
// Reveals all levels and removes all "more"-related elements for a grid's row. |
|
5524
|
|
|
// `row` is a row number. |
|
5525
|
|
|
unlimitRow: function(row) { |
|
5526
|
|
|
var rowStruct = this.rowStructs[row]; |
|
5527
|
|
|
|
|
5528
|
|
|
if (rowStruct.moreEls) { |
|
5529
|
|
|
rowStruct.moreEls.remove(); |
|
5530
|
|
|
rowStruct.moreEls = null; |
|
5531
|
|
|
} |
|
5532
|
|
|
|
|
5533
|
|
|
if (rowStruct.limitedEls) { |
|
5534
|
|
|
rowStruct.limitedEls.removeClass('fc-limited'); |
|
5535
|
|
|
rowStruct.limitedEls = null; |
|
5536
|
|
|
} |
|
5537
|
|
|
}, |
|
5538
|
|
|
|
|
5539
|
|
|
|
|
5540
|
|
|
// Renders an <a> element that represents hidden event element for a cell. |
|
5541
|
|
|
// Responsible for attaching click handler as well. |
|
5542
|
|
|
renderMoreLink: function(cell, hiddenSegs) { |
|
5543
|
|
|
var _this = this; |
|
5544
|
|
|
var view = this.view; |
|
5545
|
|
|
|
|
5546
|
|
|
return $('<a class="fc-more"/>') |
|
5547
|
|
|
.text( |
|
5548
|
|
|
this.getMoreLinkText(hiddenSegs.length) |
|
5549
|
|
|
) |
|
5550
|
|
|
.on('click', function(ev) { |
|
5551
|
|
|
var clickOption = view.opt('eventLimitClick'); |
|
5552
|
|
|
var date = view.cellToDate(cell); |
|
5553
|
|
|
var moreEl = $(this); |
|
5554
|
|
|
var dayEl = _this.getCellDayEl(cell); |
|
5555
|
|
|
var allSegs = _this.getCellSegs(cell); |
|
5556
|
|
|
|
|
5557
|
|
|
// rescope the segments to be within the cell's date |
|
5558
|
|
|
var reslicedAllSegs = _this.resliceDaySegs(allSegs, date); |
|
5559
|
|
|
var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date); |
|
5560
|
|
|
|
|
5561
|
|
|
if (typeof clickOption === 'function') { |
|
5562
|
|
|
// the returned value can be an atomic option |
|
5563
|
|
|
clickOption = view.trigger('eventLimitClick', null, { |
|
5564
|
|
|
date: date, |
|
5565
|
|
|
dayEl: dayEl, |
|
5566
|
|
|
moreEl: moreEl, |
|
5567
|
|
|
segs: reslicedAllSegs, |
|
5568
|
|
|
hiddenSegs: reslicedHiddenSegs |
|
5569
|
|
|
}, ev); |
|
5570
|
|
|
} |
|
5571
|
|
|
|
|
5572
|
|
|
if (clickOption === 'popover') { |
|
5573
|
|
|
_this.showSegPopover(date, cell, moreEl, reslicedAllSegs); |
|
5574
|
|
|
} |
|
5575
|
|
|
else if (typeof clickOption === 'string') { // a view name |
|
5576
|
|
|
view.calendar.zoomTo(date, clickOption); |
|
5577
|
|
|
} |
|
5578
|
|
|
}); |
|
5579
|
|
|
}, |
|
5580
|
|
|
|
|
5581
|
|
|
|
|
5582
|
|
|
// Reveals the popover that displays all events within a cell |
|
5583
|
|
|
showSegPopover: function(date, cell, moreLink, segs) { |
|
5584
|
|
|
var _this = this; |
|
5585
|
|
|
var view = this.view; |
|
5586
|
|
|
var moreWrap = moreLink.parent(); // the <div> wrapper around the <a> |
|
5587
|
|
|
var topEl; // the element we want to match the top coordinate of |
|
5588
|
|
|
var options; |
|
5589
|
|
|
|
|
5590
|
|
|
if (view.rowCnt == 1) { |
|
5591
|
|
|
topEl = this.view.el; // will cause the popover to cover any sort of header |
|
5592
|
|
|
} |
|
5593
|
|
|
else { |
|
5594
|
|
|
topEl = this.rowEls.eq(cell.row); // will align with top of row |
|
5595
|
|
|
} |
|
5596
|
|
|
|
|
5597
|
|
|
options = { |
|
5598
|
|
|
className: 'fc-more-popover', |
|
5599
|
|
|
content: this.renderSegPopoverContent(date, segs), |
|
5600
|
|
|
parentEl: this.el, |
|
5601
|
|
|
top: topEl.offset().top, |
|
5602
|
|
|
autoHide: true, // when the user clicks elsewhere, hide the popover |
|
5603
|
|
|
viewportConstrain: view.opt('popoverViewportConstrain'), |
|
5604
|
|
|
hide: function() { |
|
5605
|
|
|
// destroy everything when the popover is hidden |
|
5606
|
|
|
_this.segPopover.destroy(); |
|
5607
|
|
|
_this.segPopover = null; |
|
5608
|
|
|
_this.popoverSegs = null; |
|
5609
|
|
|
} |
|
5610
|
|
|
}; |
|
5611
|
|
|
|
|
5612
|
|
|
// Determine horizontal coordinate. |
|
5613
|
|
|
// We use the moreWrap instead of the <td> to avoid border confusion. |
|
5614
|
|
|
if (view.opt('isRTL')) { |
|
5615
|
|
|
options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border |
|
5616
|
|
|
} |
|
5617
|
|
|
else { |
|
5618
|
|
|
options.left = moreWrap.offset().left - 1; // -1 to be over cell border |
|
5619
|
|
|
} |
|
5620
|
|
|
|
|
5621
|
|
|
this.segPopover = new Popover(options); |
|
5622
|
|
|
this.segPopover.show(); |
|
5623
|
|
|
}, |
|
5624
|
|
|
|
|
5625
|
|
|
|
|
5626
|
|
|
// Builds the inner DOM contents of the segment popover |
|
5627
|
|
|
renderSegPopoverContent: function(date, segs) { |
|
5628
|
|
|
var view = this.view; |
|
5629
|
|
|
var isTheme = view.opt('theme'); |
|
5630
|
|
|
var title = date.format(view.opt('dayPopoverFormat')); |
|
5631
|
|
|
var content = $( |
|
5632
|
|
|
'<div class="fc-header ' + view.widgetHeaderClass + '">' + |
|
5633
|
|
|
'<span class="fc-close ' + |
|
5634
|
|
|
(isTheme ? 'ui-icon ui-icon-closethick' : 'fc-icon fc-icon-x') + |
|
5635
|
|
|
'"></span>' + |
|
5636
|
|
|
'<span class="fc-title">' + |
|
5637
|
|
|
htmlEscape(title) + |
|
5638
|
|
|
'</span>' + |
|
5639
|
|
|
'<div class="fc-clear"/>' + |
|
5640
|
|
|
'</div>' + |
|
5641
|
|
|
'<div class="fc-body ' + view.widgetContentClass + '">' + |
|
5642
|
|
|
'<div class="fc-event-container"></div>' + |
|
5643
|
|
|
'</div>' |
|
5644
|
|
|
); |
|
5645
|
|
|
var segContainer = content.find('.fc-event-container'); |
|
5646
|
|
|
var i; |
|
5647
|
|
|
|
|
5648
|
|
|
// render each seg's `el` and only return the visible segs |
|
5649
|
|
|
segs = this.renderSegs(segs, true); // disableResizing=true |
|
5650
|
|
|
this.popoverSegs = segs; |
|
5651
|
|
|
|
|
5652
|
|
|
for (i = 0; i < segs.length; i++) { |
|
5653
|
|
|
|
|
5654
|
|
|
// because segments in the popover are not part of a grid coordinate system, provide a hint to any |
|
5655
|
|
|
// grids that want to do drag-n-drop about which cell it came from |
|
5656
|
|
|
segs[i].cellDate = date; |
|
5657
|
|
|
|
|
5658
|
|
|
segContainer.append(segs[i].el); |
|
5659
|
|
|
} |
|
5660
|
|
|
|
|
5661
|
|
|
return content; |
|
5662
|
|
|
}, |
|
5663
|
|
|
|
|
5664
|
|
|
|
|
5665
|
|
|
// Given the events within an array of segment objects, reslice them to be in a single day |
|
5666
|
|
|
resliceDaySegs: function(segs, dayDate) { |
|
5667
|
|
|
var events = $.map(segs, function(seg) { |
|
5668
|
|
|
return seg.event; |
|
5669
|
|
|
}); |
|
5670
|
|
|
var dayStart = dayDate.clone().stripTime(); |
|
5671
|
|
|
var dayEnd = dayStart.clone().add(1, 'days'); |
|
5672
|
|
|
|
|
5673
|
|
|
return this.eventsToSegs(events, dayStart, dayEnd); |
|
5674
|
|
|
}, |
|
5675
|
|
|
|
|
5676
|
|
|
|
|
5677
|
|
|
// Generates the text that should be inside a "more" link, given the number of events it represents |
|
5678
|
|
|
getMoreLinkText: function(num) { |
|
5679
|
|
|
var view = this.view; |
|
5680
|
|
|
var opt = view.opt('eventLimitText'); |
|
5681
|
|
|
|
|
5682
|
|
|
if (typeof opt === 'function') { |
|
5683
|
|
|
return opt(num); |
|
5684
|
|
|
} |
|
5685
|
|
|
else { |
|
5686
|
|
|
return '+' + num + ' ' + opt; |
|
5687
|
|
|
} |
|
5688
|
|
|
}, |
|
5689
|
|
|
|
|
5690
|
|
|
|
|
5691
|
|
|
// Returns segments within a given cell. |
|
5692
|
|
|
// If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs. |
|
5693
|
|
|
getCellSegs: function(cell, startLevel) { |
|
5694
|
|
|
var segMatrix = this.rowStructs[cell.row].segMatrix; |
|
5695
|
|
|
var level = startLevel || 0; |
|
5696
|
|
|
var segs = []; |
|
5697
|
|
|
var seg; |
|
5698
|
|
|
|
|
5699
|
|
|
while (level < segMatrix.length) { |
|
5700
|
|
|
seg = segMatrix[level][cell.col]; |
|
5701
|
|
|
if (seg) { |
|
5702
|
|
|
segs.push(seg); |
|
5703
|
|
|
} |
|
5704
|
|
|
level++; |
|
5705
|
|
|
} |
|
5706
|
|
|
|
|
5707
|
|
|
return segs; |
|
5708
|
|
|
} |
|
5709
|
|
|
|
|
5710
|
|
|
}); |
|
5711
|
|
|
|
|
5712
|
|
|
;; |
|
5713
|
|
|
|
|
5714
|
|
|
/* A component that renders one or more columns of vertical time slots |
|
5715
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
|
5716
|
|
|
|
|
5717
|
|
|
function TimeGrid(view) { |
|
5718
|
|
|
Grid.call(this, view); // call the super-constructor |
|
5719
|
|
|
} |
|
5720
|
|
|
|
|
5721
|
|
|
|
|
5722
|
|
|
TimeGrid.prototype = createObject(Grid.prototype); // define the super-class |
|
5723
|
|
|
$.extend(TimeGrid.prototype, { |
|
5724
|
|
|
|
|
5725
|
|
|
slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines |
|
5726
|
|
|
snapDuration: null, // granularity of time for dragging and selecting |
|
5727
|
|
|
|
|
5728
|
|
|
minTime: null, // Duration object that denotes the first visible time of any given day |
|
5729
|
|
|
maxTime: null, // Duration object that denotes the exclusive visible end time of any given day |
|
5730
|
|
|
|
|
5731
|
|
|
dayEls: null, // cells elements in the day-row background |
|
5732
|
|
|
slatEls: null, // elements running horizontally across all columns |
|
5733
|
|
|
|
|
5734
|
|
|
slatTops: null, // an array of top positions, relative to the container. last item holds bottom of last slot |
|
5735
|
|
|
|
|
5736
|
|
|
highlightEl: null, // cell skeleton element for rendering the highlight |
|
5737
|
|
|
helperEl: null, // cell skeleton element for rendering the mock event "helper" |
|
5738
|
|
|
|
|
5739
|
|
|
|
|
5740
|
|
|
// Renders the time grid into `this.el`, which should already be assigned. |
|
5741
|
|
|
// Relies on the view's colCnt. In the future, this component should probably be self-sufficient. |
|
5742
|
|
|
render: function() { |
|
5743
|
|
|
this.processOptions(); |
|
5744
|
|
|
|
|
5745
|
|
|
this.el.html(this.renderHtml()); |
|
5746
|
|
|
|
|
5747
|
|
|
this.dayEls = this.el.find('.fc-day'); |
|
5748
|
|
|
this.slatEls = this.el.find('.fc-slats tr'); |
|
5749
|
|
|
|
|
5750
|
|
|
this.computeSlatTops(); |
|
5751
|
|
|
|
|
5752
|
|
|
Grid.prototype.render.call(this); // call the super-method |
|
5753
|
|
|
}, |
|
5754
|
|
|
|
|
5755
|
|
|
|
|
5756
|
|
|
// Renders the basic HTML skeleton for the grid |
|
5757
|
|
|
renderHtml: function() { |
|
5758
|
|
|
return '' + |
|
5759
|
|
|
'<div class="fc-bg">' + |
|
5760
|
|
|
'<table>' + |
|
5761
|
|
|
this.rowHtml('slotBg') + // leverages RowRenderer, which will call slotBgCellHtml |
|
5762
|
|
|
'</table>' + |
|
5763
|
|
|
'</div>' + |
|
5764
|
|
|
'<div class="fc-slats">' + |
|
5765
|
|
|
'<table>' + |
|
5766
|
|
|
this.slatRowHtml() + |
|
5767
|
|
|
'</table>' + |
|
5768
|
|
|
'</div>'; |
|
5769
|
|
|
}, |
|
5770
|
|
|
|
|
5771
|
|
|
|
|
5772
|
|
|
// Renders the HTML for a vertical background cell behind the slots. |
|
5773
|
|
|
// This method is distinct from 'bg' because we wanted a new `rowType` so the View could customize the rendering. |
|
5774
|
|
|
slotBgCellHtml: function(row, col, date) { |
|
5775
|
|
|
return this.bgCellHtml(row, col, date); |
|
5776
|
|
|
}, |
|
5777
|
|
|
|
|
5778
|
|
|
|
|
5779
|
|
|
// Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL. |
|
5780
|
|
|
slatRowHtml: function() { |
|
5781
|
|
|
var view = this.view; |
|
5782
|
|
|
var calendar = view.calendar; |
|
5783
|
|
|
var isRTL = view.opt('isRTL'); |
|
5784
|
|
|
var html = ''; |
|
5785
|
|
|
var slotNormal = this.slotDuration.asMinutes() % 15 === 0; |
|
5786
|
|
|
var slotTime = moment.duration(+this.minTime); // wish there was .clone() for durations |
|
5787
|
|
|
var slotDate; // will be on the view's first day, but we only care about its time |
|
5788
|
|
|
var minutes; |
|
5789
|
|
|
var axisHtml; |
|
5790
|
|
|
|
|
5791
|
|
|
// Calculate the time for each slot |
|
5792
|
|
|
while (slotTime < this.maxTime) { |
|
5793
|
|
|
slotDate = view.start.clone().time(slotTime); // will be in UTC but that's good. to avoid DST issues |
|
5794
|
|
|
minutes = slotDate.minutes(); |
|
5795
|
|
|
|
|
5796
|
|
|
axisHtml = |
|
5797
|
|
|
'<td class="fc-axis fc-time ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '>' + |
|
5798
|
|
|
((!slotNormal || !minutes) ? // if irregular slot duration, or on the hour, then display the time |
|
5799
|
|
|
'<span>' + // for matchCellWidths |
|
5800
|
|
|
htmlEscape(calendar.formatDate(slotDate, view.opt('axisFormat'))) + |
|
5801
|
|
|
'</span>' : |
|
5802
|
|
|
'' |
|
5803
|
|
|
) + |
|
5804
|
|
|
'</td>'; |
|
5805
|
|
|
|
|
5806
|
|
|
html += |
|
5807
|
|
|
'<tr ' + (!minutes ? '' : 'class="fc-minor"') + '>' + |
|
5808
|
|
|
(!isRTL ? axisHtml : '') + |
|
5809
|
|
|
'<td class="' + view.widgetContentClass + '"/>' + |
|
5810
|
|
|
(isRTL ? axisHtml : '') + |
|
5811
|
|
|
"</tr>"; |
|
5812
|
|
|
|
|
5813
|
|
|
slotTime.add(this.slotDuration); |
|
5814
|
|
|
} |
|
5815
|
|
|
|
|
5816
|
|
|
return html; |
|
5817
|
|
|
}, |
|
5818
|
|
|
|
|
5819
|
|
|
|
|
5820
|
|
|
// Parses various options into properties of this object |
|
5821
|
|
|
processOptions: function() { |
|
5822
|
|
|
var view = this.view; |
|
5823
|
|
|
var slotDuration = view.opt('slotDuration'); |
|
5824
|
|
|
var snapDuration = view.opt('snapDuration'); |
|
5825
|
|
|
|
|
5826
|
|
|
slotDuration = moment.duration(slotDuration); |
|
5827
|
|
|
snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration; |
|
5828
|
|
|
|
|
5829
|
|
|
this.slotDuration = slotDuration; |
|
5830
|
|
|
this.snapDuration = snapDuration; |
|
5831
|
|
|
this.cellDuration = snapDuration; // important to assign this for Grid.events.js |
|
5832
|
|
|
|
|
5833
|
|
|
this.minTime = moment.duration(view.opt('minTime')); |
|
5834
|
|
|
this.maxTime = moment.duration(view.opt('maxTime')); |
|
5835
|
|
|
}, |
|
5836
|
|
|
|
|
5837
|
|
|
|
|
5838
|
|
|
// Slices up a date range into a segment for each column |
|
5839
|
|
|
rangeToSegs: function(rangeStart, rangeEnd) { |
|
5840
|
|
|
var view = this.view; |
|
5841
|
|
|
var segs = []; |
|
5842
|
|
|
var seg; |
|
5843
|
|
|
var col; |
|
5844
|
|
|
var cellDate; |
|
5845
|
|
|
var colStart, colEnd; |
|
5846
|
|
|
|
|
5847
|
|
|
// normalize |
|
5848
|
|
|
rangeStart = rangeStart.clone().stripZone(); |
|
5849
|
|
|
rangeEnd = rangeEnd.clone().stripZone(); |
|
5850
|
|
|
|
|
5851
|
|
|
for (col = 0; col < view.colCnt; col++) { |
|
5852
|
|
|
cellDate = view.cellToDate(0, col); // use the View's cell system for this |
|
5853
|
|
|
colStart = cellDate.clone().time(this.minTime); |
|
5854
|
|
|
colEnd = cellDate.clone().time(this.maxTime); |
|
5855
|
|
|
seg = intersectionToSeg(rangeStart, rangeEnd, colStart, colEnd); |
|
5856
|
|
|
if (seg) { |
|
5857
|
|
|
seg.col = col; |
|
5858
|
|
|
segs.push(seg); |
|
5859
|
|
|
} |
|
5860
|
|
|
} |
|
5861
|
|
|
|
|
5862
|
|
|
return segs; |
|
5863
|
|
|
}, |
|
5864
|
|
|
|
|
5865
|
|
|
|
|
5866
|
|
|
/* Coordinates |
|
5867
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
5868
|
|
|
|
|
5869
|
|
|
|
|
5870
|
|
|
// Called when there is a window resize/zoom and we need to recalculate coordinates for the grid |
|
5871
|
|
|
resize: function() { |
|
5872
|
|
|
this.computeSlatTops(); |
|
5873
|
|
|
this.updateSegVerticals(); |
|
5874
|
|
|
}, |
|
5875
|
|
|
|
|
5876
|
|
|
|
|
5877
|
|
|
// Populates the given empty `rows` and `cols` arrays with offset positions of the "snap" cells. |
|
5878
|
|
|
// "Snap" cells are different the slots because they might have finer granularity. |
|
5879
|
|
|
buildCoords: function(rows, cols) { |
|
5880
|
|
|
var colCnt = this.view.colCnt; |
|
5881
|
|
|
var originTop = this.el.offset().top; |
|
5882
|
|
|
var snapTime = moment.duration(+this.minTime); |
|
5883
|
|
|
var p = null; |
|
5884
|
|
|
var e, n; |
|
5885
|
|
|
|
|
5886
|
|
|
this.dayEls.slice(0, colCnt).each(function(i, _e) { |
|
5887
|
|
|
e = $(_e); |
|
5888
|
|
|
n = e.offset().left; |
|
5889
|
|
|
if (p) { |
|
5890
|
|
|
p[1] = n; |
|
5891
|
|
|
} |
|
5892
|
|
|
p = [ n ]; |
|
5893
|
|
|
cols[i] = p; |
|
5894
|
|
|
}); |
|
5895
|
|
|
p[1] = n + e.outerWidth(); |
|
5896
|
|
|
|
|
5897
|
|
|
p = null; |
|
5898
|
|
|
while (snapTime < this.maxTime) { |
|
5899
|
|
|
n = originTop + this.computeTimeTop(snapTime); |
|
5900
|
|
|
if (p) { |
|
5901
|
|
|
p[1] = n; |
|
5902
|
|
|
} |
|
5903
|
|
|
p = [ n ]; |
|
5904
|
|
|
rows.push(p); |
|
5905
|
|
|
snapTime.add(this.snapDuration); |
|
5906
|
|
|
} |
|
5907
|
|
|
p[1] = originTop + this.computeTimeTop(snapTime); // the position of the exclusive end |
|
5908
|
|
|
}, |
|
5909
|
|
|
|
|
5910
|
|
|
|
|
5911
|
|
|
// Gets the datetime for the given slot cell |
|
5912
|
|
|
getCellDate: function(cell) { |
|
5913
|
|
|
var view = this.view; |
|
5914
|
|
|
var calendar = view.calendar; |
|
5915
|
|
|
|
|
5916
|
|
|
return calendar.rezoneDate( // since we are adding a time, it needs to be in the calendar's timezone |
|
5917
|
|
|
view.cellToDate(0, cell.col) // View's coord system only accounts for start-of-day for column |
|
5918
|
|
|
.time(this.minTime + this.snapDuration * cell.row) |
|
5919
|
|
|
); |
|
5920
|
|
|
}, |
|
5921
|
|
|
|
|
5922
|
|
|
|
|
5923
|
|
|
// Gets the element that represents the whole-day the cell resides on |
|
5924
|
|
|
getCellDayEl: function(cell) { |
|
5925
|
|
|
return this.dayEls.eq(cell.col); |
|
5926
|
|
|
}, |
|
5927
|
|
|
|
|
5928
|
|
|
|
|
5929
|
|
|
// Computes the top coordinate, relative to the bounds of the grid, of the given date. |
|
5930
|
|
|
// A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight. |
|
5931
|
|
|
computeDateTop: function(date, startOfDayDate) { |
|
5932
|
|
|
return this.computeTimeTop( |
|
5933
|
|
|
moment.duration( |
|
5934
|
|
|
date.clone().stripZone() - startOfDayDate.clone().stripTime() |
|
5935
|
|
|
) |
|
5936
|
|
|
); |
|
5937
|
|
|
}, |
|
5938
|
|
|
|
|
5939
|
|
|
|
|
5940
|
|
|
// Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration). |
|
5941
|
|
|
computeTimeTop: function(time) { |
|
5942
|
|
|
var slatCoverage = (time - this.minTime) / this.slotDuration; // floating-point value of # of slots covered |
|
5943
|
|
|
var slatIndex; |
|
5944
|
|
|
var slatRemainder; |
|
5945
|
|
|
var slatTop; |
|
5946
|
|
|
var slatBottom; |
|
5947
|
|
|
|
|
5948
|
|
|
// constrain. because minTime/maxTime might be customized |
|
5949
|
|
|
slatCoverage = Math.max(0, slatCoverage); |
|
5950
|
|
|
slatCoverage = Math.min(this.slatEls.length, slatCoverage); |
|
5951
|
|
|
|
|
5952
|
|
|
slatIndex = Math.floor(slatCoverage); // an integer index of the furthest whole slot |
|
5953
|
|
|
slatRemainder = slatCoverage - slatIndex; |
|
5954
|
|
|
slatTop = this.slatTops[slatIndex]; // the top position of the furthest whole slot |
|
5955
|
|
|
|
|
5956
|
|
|
if (slatRemainder) { // time spans part-way into the slot |
|
5957
|
|
|
slatBottom = this.slatTops[slatIndex + 1]; |
|
5958
|
|
|
return slatTop + (slatBottom - slatTop) * slatRemainder; // part-way between slots |
|
5959
|
|
|
} |
|
5960
|
|
|
else { |
|
5961
|
|
|
return slatTop; |
|
5962
|
|
|
} |
|
5963
|
|
|
}, |
|
5964
|
|
|
|
|
5965
|
|
|
|
|
5966
|
|
|
// Queries each `slatEl` for its position relative to the grid's container and stores it in `slatTops`. |
|
5967
|
|
|
// Includes the the bottom of the last slat as the last item in the array. |
|
5968
|
|
|
computeSlatTops: function() { |
|
5969
|
|
|
var tops = []; |
|
5970
|
|
|
var top; |
|
5971
|
|
|
|
|
5972
|
|
|
this.slatEls.each(function(i, node) { |
|
5973
|
|
|
top = $(node).position().top; |
|
5974
|
|
|
tops.push(top); |
|
5975
|
|
|
}); |
|
5976
|
|
|
|
|
5977
|
|
|
tops.push(top + this.slatEls.last().outerHeight()); // bottom of the last slat |
|
5978
|
|
|
|
|
5979
|
|
|
this.slatTops = tops; |
|
5980
|
|
|
}, |
|
5981
|
|
|
|
|
5982
|
|
|
|
|
5983
|
|
|
/* Event Drag Visualization |
|
5984
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
5985
|
|
|
|
|
5986
|
|
|
|
|
5987
|
|
|
// Renders a visual indication of an event being dragged over the specified date(s). |
|
5988
|
|
|
// `end` and `seg` can be null. See View's documentation on renderDrag for more info. |
|
5989
|
|
|
renderDrag: function(start, end, seg) { |
|
5990
|
|
|
var opacity; |
|
5991
|
|
|
|
|
5992
|
|
|
if (seg) { // if there is event information for this drag, render a helper event |
|
5993
|
|
|
this.renderRangeHelper(start, end, seg); |
|
5994
|
|
|
|
|
5995
|
|
|
opacity = this.view.opt('dragOpacity'); |
|
5996
|
|
|
if (opacity !== undefined) { |
|
5997
|
|
|
this.helperEl.css('opacity', opacity); |
|
5998
|
|
|
} |
|
5999
|
|
|
|
|
6000
|
|
|
return true; // signal that a helper has been rendered |
|
6001
|
|
|
} |
|
6002
|
|
|
else { |
|
6003
|
|
|
// otherwise, just render a highlight |
|
6004
|
|
|
this.renderHighlight( |
|
6005
|
|
|
start, |
|
6006
|
|
|
end || this.view.calendar.getDefaultEventEnd(false, start) |
|
6007
|
|
|
); |
|
|
|
|
|
|
6008
|
|
|
} |
|
6009
|
|
|
}, |
|
6010
|
|
|
|
|
6011
|
|
|
|
|
6012
|
|
|
// Unrenders any visual indication of an event being dragged |
|
6013
|
|
|
destroyDrag: function() { |
|
6014
|
|
|
this.destroyHelper(); |
|
6015
|
|
|
this.destroyHighlight(); |
|
6016
|
|
|
}, |
|
6017
|
|
|
|
|
6018
|
|
|
|
|
6019
|
|
|
/* Event Resize Visualization |
|
6020
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
6021
|
|
|
|
|
6022
|
|
|
|
|
6023
|
|
|
// Renders a visual indication of an event being resized |
|
6024
|
|
|
renderResize: function(start, end, seg) { |
|
6025
|
|
|
this.renderRangeHelper(start, end, seg); |
|
6026
|
|
|
}, |
|
6027
|
|
|
|
|
6028
|
|
|
|
|
6029
|
|
|
// Unrenders any visual indication of an event being resized |
|
6030
|
|
|
destroyResize: function() { |
|
6031
|
|
|
this.destroyHelper(); |
|
6032
|
|
|
}, |
|
6033
|
|
|
|
|
6034
|
|
|
|
|
6035
|
|
|
/* Event Helper |
|
6036
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
6037
|
|
|
|
|
6038
|
|
|
|
|
6039
|
|
|
// Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag) |
|
6040
|
|
|
renderHelper: function(event, sourceSeg) { |
|
6041
|
|
|
var res = this.renderEventTable([ event ]); |
|
6042
|
|
|
var tableEl = res.tableEl; |
|
6043
|
|
|
var segs = res.segs; |
|
6044
|
|
|
var i, seg; |
|
6045
|
|
|
var sourceEl; |
|
6046
|
|
|
|
|
6047
|
|
|
// Try to make the segment that is in the same row as sourceSeg look the same |
|
6048
|
|
|
for (i = 0; i < segs.length; i++) { |
|
6049
|
|
|
seg = segs[i]; |
|
6050
|
|
|
if (sourceSeg && sourceSeg.col === seg.col) { |
|
6051
|
|
|
sourceEl = sourceSeg.el; |
|
6052
|
|
|
seg.el.css({ |
|
6053
|
|
|
left: sourceEl.css('left'), |
|
6054
|
|
|
right: sourceEl.css('right'), |
|
6055
|
|
|
'margin-left': sourceEl.css('margin-left'), |
|
6056
|
|
|
'margin-right': sourceEl.css('margin-right') |
|
6057
|
|
|
}); |
|
6058
|
|
|
} |
|
6059
|
|
|
} |
|
6060
|
|
|
|
|
6061
|
|
|
this.helperEl = $('<div class="fc-helper-skeleton"/>') |
|
6062
|
|
|
.append(tableEl) |
|
6063
|
|
|
.appendTo(this.el); |
|
6064
|
|
|
}, |
|
6065
|
|
|
|
|
6066
|
|
|
|
|
6067
|
|
|
// Unrenders any mock helper event |
|
6068
|
|
|
destroyHelper: function() { |
|
6069
|
|
|
if (this.helperEl) { |
|
6070
|
|
|
this.helperEl.remove(); |
|
6071
|
|
|
this.helperEl = null; |
|
6072
|
|
|
} |
|
6073
|
|
|
}, |
|
6074
|
|
|
|
|
6075
|
|
|
|
|
6076
|
|
|
/* Selection |
|
6077
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
6078
|
|
|
|
|
6079
|
|
|
|
|
6080
|
|
|
// Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight. |
|
6081
|
|
|
renderSelection: function(start, end) { |
|
6082
|
|
|
if (this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered |
|
6083
|
|
|
this.renderRangeHelper(start, end); |
|
6084
|
|
|
} |
|
6085
|
|
|
else { |
|
6086
|
|
|
this.renderHighlight(start, end); |
|
6087
|
|
|
} |
|
6088
|
|
|
}, |
|
6089
|
|
|
|
|
6090
|
|
|
|
|
6091
|
|
|
// Unrenders any visual indication of a selection |
|
6092
|
|
|
destroySelection: function() { |
|
6093
|
|
|
this.destroyHelper(); |
|
6094
|
|
|
this.destroyHighlight(); |
|
6095
|
|
|
}, |
|
6096
|
|
|
|
|
6097
|
|
|
|
|
6098
|
|
|
/* Highlight |
|
6099
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
6100
|
|
|
|
|
6101
|
|
|
|
|
6102
|
|
|
// Renders an emphasis on the given date range. `start` is inclusive. `end` is exclusive. |
|
6103
|
|
|
renderHighlight: function(start, end) { |
|
6104
|
|
|
this.highlightEl = $( |
|
6105
|
|
|
this.highlightSkeletonHtml(start, end) |
|
6106
|
|
|
).appendTo(this.el); |
|
6107
|
|
|
}, |
|
6108
|
|
|
|
|
6109
|
|
|
|
|
6110
|
|
|
// Unrenders the emphasis on a date range |
|
6111
|
|
|
destroyHighlight: function() { |
|
6112
|
|
|
if (this.highlightEl) { |
|
6113
|
|
|
this.highlightEl.remove(); |
|
6114
|
|
|
this.highlightEl = null; |
|
6115
|
|
|
} |
|
6116
|
|
|
}, |
|
6117
|
|
|
|
|
6118
|
|
|
|
|
6119
|
|
|
// Generates HTML for a table element with containers in each column, responsible for absolutely positioning the |
|
6120
|
|
|
// highlight elements to cover the highlighted slots. |
|
6121
|
|
|
highlightSkeletonHtml: function(start, end) { |
|
6122
|
|
|
var view = this.view; |
|
6123
|
|
|
var segs = this.rangeToSegs(start, end); |
|
6124
|
|
|
var cellHtml = ''; |
|
6125
|
|
|
var col = 0; |
|
6126
|
|
|
var i, seg; |
|
6127
|
|
|
var dayDate; |
|
6128
|
|
|
var top, bottom; |
|
6129
|
|
|
|
|
6130
|
|
|
for (i = 0; i < segs.length; i++) { // loop through the segments. one per column |
|
6131
|
|
|
seg = segs[i]; |
|
6132
|
|
|
|
|
6133
|
|
|
// need empty cells beforehand? |
|
6134
|
|
|
if (col < seg.col) { |
|
6135
|
|
|
cellHtml += '<td colspan="' + (seg.col - col) + '"/>'; |
|
6136
|
|
|
col = seg.col; |
|
6137
|
|
|
} |
|
6138
|
|
|
|
|
6139
|
|
|
// compute vertical position |
|
6140
|
|
|
dayDate = view.cellToDate(0, col); |
|
6141
|
|
|
top = this.computeDateTop(seg.start, dayDate); |
|
6142
|
|
|
bottom = this.computeDateTop(seg.end, dayDate); // the y position of the bottom edge |
|
6143
|
|
|
|
|
6144
|
|
|
// generate the cell HTML. bottom becomes negative because it needs to be a CSS value relative to the |
|
6145
|
|
|
// bottom edge of the zero-height container. |
|
6146
|
|
|
cellHtml += |
|
6147
|
|
|
'<td>' + |
|
6148
|
|
|
'<div class="fc-highlight-container">' + |
|
6149
|
|
|
'<div class="fc-highlight" style="top:' + top + 'px;bottom:-' + bottom + 'px"/>' + |
|
6150
|
|
|
'</div>' + |
|
6151
|
|
|
'</td>'; |
|
6152
|
|
|
|
|
6153
|
|
|
col++; |
|
6154
|
|
|
} |
|
6155
|
|
|
|
|
6156
|
|
|
// need empty cells after the last segment? |
|
6157
|
|
|
if (col < view.colCnt) { |
|
6158
|
|
|
cellHtml += '<td colspan="' + (view.colCnt - col) + '"/>'; |
|
6159
|
|
|
} |
|
6160
|
|
|
|
|
6161
|
|
|
cellHtml = this.bookendCells(cellHtml, 'highlight'); |
|
6162
|
|
|
|
|
6163
|
|
|
return '' + |
|
6164
|
|
|
'<div class="fc-highlight-skeleton">' + |
|
6165
|
|
|
'<table>' + |
|
6166
|
|
|
'<tr>' + |
|
6167
|
|
|
cellHtml + |
|
6168
|
|
|
'</tr>' + |
|
6169
|
|
|
'</table>' + |
|
6170
|
|
|
'</div>'; |
|
6171
|
|
|
} |
|
6172
|
|
|
|
|
6173
|
|
|
}); |
|
6174
|
|
|
|
|
6175
|
|
|
;; |
|
6176
|
|
|
|
|
6177
|
|
|
/* Event-rendering methods for the TimeGrid class |
|
6178
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
|
6179
|
|
|
|
|
6180
|
|
|
$.extend(TimeGrid.prototype, { |
|
6181
|
|
|
|
|
6182
|
|
|
segs: null, // segment objects rendered in the component. null of events haven't been rendered yet |
|
6183
|
|
|
eventSkeletonEl: null, // has cells with event-containers, which contain absolutely positioned event elements |
|
6184
|
|
|
|
|
6185
|
|
|
|
|
6186
|
|
|
// Renders the events onto the grid and returns an array of segments that have been rendered |
|
6187
|
|
|
renderEvents: function(events) { |
|
6188
|
|
|
var res = this.renderEventTable(events); |
|
6189
|
|
|
|
|
6190
|
|
|
this.eventSkeletonEl = $('<div class="fc-content-skeleton"/>').append(res.tableEl); |
|
6191
|
|
|
this.el.append(this.eventSkeletonEl); |
|
6192
|
|
|
|
|
6193
|
|
|
this.segs = res.segs; |
|
6194
|
|
|
}, |
|
6195
|
|
|
|
|
6196
|
|
|
|
|
6197
|
|
|
// Retrieves rendered segment objects |
|
6198
|
|
|
getSegs: function() { |
|
6199
|
|
|
return this.segs || []; |
|
6200
|
|
|
}, |
|
6201
|
|
|
|
|
6202
|
|
|
|
|
6203
|
|
|
// Removes all event segment elements from the view |
|
6204
|
|
|
destroyEvents: function() { |
|
6205
|
|
|
Grid.prototype.destroyEvents.call(this); // call the super-method |
|
6206
|
|
|
|
|
6207
|
|
|
if (this.eventSkeletonEl) { |
|
6208
|
|
|
this.eventSkeletonEl.remove(); |
|
6209
|
|
|
this.eventSkeletonEl = null; |
|
6210
|
|
|
} |
|
6211
|
|
|
|
|
6212
|
|
|
this.segs = null; |
|
6213
|
|
|
}, |
|
6214
|
|
|
|
|
6215
|
|
|
|
|
6216
|
|
|
// Renders and returns the <table> portion of the event-skeleton. |
|
6217
|
|
|
// Returns an object with properties 'tbodyEl' and 'segs'. |
|
6218
|
|
|
renderEventTable: function(events) { |
|
6219
|
|
|
var tableEl = $('<table><tr/></table>'); |
|
6220
|
|
|
var trEl = tableEl.find('tr'); |
|
6221
|
|
|
var segs = this.eventsToSegs(events); |
|
6222
|
|
|
var segCols; |
|
6223
|
|
|
var i, seg; |
|
6224
|
|
|
var col, colSegs; |
|
6225
|
|
|
var containerEl; |
|
6226
|
|
|
|
|
6227
|
|
|
segs = this.renderSegs(segs); // returns only the visible segs |
|
6228
|
|
|
segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg |
|
6229
|
|
|
|
|
6230
|
|
|
this.computeSegVerticals(segs); // compute and assign top/bottom |
|
6231
|
|
|
|
|
6232
|
|
|
for (col = 0; col < segCols.length; col++) { // iterate each column grouping |
|
6233
|
|
|
colSegs = segCols[col]; |
|
6234
|
|
|
placeSlotSegs(colSegs); // compute horizontal coordinates, z-index's, and reorder the array |
|
6235
|
|
|
|
|
6236
|
|
|
containerEl = $('<div class="fc-event-container"/>'); |
|
6237
|
|
|
|
|
6238
|
|
|
// assign positioning CSS and insert into container |
|
6239
|
|
|
for (i = 0; i < colSegs.length; i++) { |
|
6240
|
|
|
seg = colSegs[i]; |
|
6241
|
|
|
seg.el.css(this.generateSegPositionCss(seg)); |
|
6242
|
|
|
|
|
6243
|
|
|
// if the height is short, add a className for alternate styling |
|
6244
|
|
|
if (seg.bottom - seg.top < 30) { |
|
6245
|
|
|
seg.el.addClass('fc-short'); |
|
6246
|
|
|
} |
|
6247
|
|
|
|
|
6248
|
|
|
containerEl.append(seg.el); |
|
6249
|
|
|
} |
|
6250
|
|
|
|
|
6251
|
|
|
trEl.append($('<td/>').append(containerEl)); |
|
6252
|
|
|
} |
|
6253
|
|
|
|
|
6254
|
|
|
this.bookendCells(trEl, 'eventSkeleton'); |
|
6255
|
|
|
|
|
6256
|
|
|
return { |
|
6257
|
|
|
tableEl: tableEl, |
|
6258
|
|
|
segs: segs |
|
6259
|
|
|
}; |
|
6260
|
|
|
}, |
|
6261
|
|
|
|
|
6262
|
|
|
|
|
6263
|
|
|
// Refreshes the CSS top/bottom coordinates for each segment element. Probably after a window resize/zoom. |
|
6264
|
|
|
updateSegVerticals: function() { |
|
6265
|
|
|
var segs = this.segs; |
|
6266
|
|
|
var i; |
|
6267
|
|
|
|
|
6268
|
|
|
if (segs) { |
|
6269
|
|
|
this.computeSegVerticals(segs); |
|
6270
|
|
|
|
|
6271
|
|
|
for (i = 0; i < segs.length; i++) { |
|
6272
|
|
|
segs[i].el.css( |
|
6273
|
|
|
this.generateSegVerticalCss(segs[i]) |
|
6274
|
|
|
); |
|
6275
|
|
|
} |
|
6276
|
|
|
} |
|
6277
|
|
|
}, |
|
6278
|
|
|
|
|
6279
|
|
|
|
|
6280
|
|
|
// For each segment in an array, computes and assigns its top and bottom properties |
|
6281
|
|
|
computeSegVerticals: function(segs) { |
|
6282
|
|
|
var i, seg; |
|
6283
|
|
|
|
|
6284
|
|
|
for (i = 0; i < segs.length; i++) { |
|
6285
|
|
|
seg = segs[i]; |
|
6286
|
|
|
seg.top = this.computeDateTop(seg.start, seg.start); |
|
6287
|
|
|
seg.bottom = this.computeDateTop(seg.end, seg.start); |
|
6288
|
|
|
} |
|
6289
|
|
|
}, |
|
6290
|
|
|
|
|
6291
|
|
|
|
|
6292
|
|
|
// Renders the HTML for a single event segment's default rendering |
|
6293
|
|
|
renderSegHtml: function(seg, disableResizing) { |
|
6294
|
|
|
var view = this.view; |
|
6295
|
|
|
var event = seg.event; |
|
6296
|
|
|
var isDraggable = view.isEventDraggable(event); |
|
6297
|
|
|
var isResizable = !disableResizing && seg.isEnd && view.isEventResizable(event); |
|
6298
|
|
|
var classes = this.getSegClasses(seg, isDraggable, isResizable); |
|
6299
|
|
|
var skinCss = this.getEventSkinCss(event); |
|
6300
|
|
|
var timeText; |
|
6301
|
|
|
var fullTimeText; // more verbose time text. for the print stylesheet |
|
6302
|
|
|
var startTimeText; // just the start time text |
|
6303
|
|
|
|
|
6304
|
|
|
classes.unshift('fc-time-grid-event'); |
|
6305
|
|
|
|
|
6306
|
|
|
if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day... |
|
6307
|
|
|
// Don't display time text on segments that run entirely through a day. |
|
6308
|
|
|
// That would appear as midnight-midnight and would look dumb. |
|
6309
|
|
|
// Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am) |
|
6310
|
|
|
if (seg.isStart || seg.isEnd) { |
|
6311
|
|
|
timeText = view.getEventTimeText(seg.start, seg.end); |
|
6312
|
|
|
fullTimeText = view.getEventTimeText(seg.start, seg.end, 'LT'); |
|
6313
|
|
|
startTimeText = view.getEventTimeText(seg.start, null); |
|
6314
|
|
|
} |
|
6315
|
|
|
} else { |
|
6316
|
|
|
// Display the normal time text for the *event's* times |
|
6317
|
|
|
timeText = view.getEventTimeText(event); |
|
6318
|
|
|
fullTimeText = view.getEventTimeText(event, 'LT'); |
|
6319
|
|
|
startTimeText = view.getEventTimeText(event.start, null); |
|
6320
|
|
|
} |
|
6321
|
|
|
|
|
6322
|
|
|
return '<a class="' + classes.join(' ') + '"' + |
|
6323
|
|
|
(event.url ? |
|
6324
|
|
|
' href="' + htmlEscape(event.url) + '"' : |
|
6325
|
|
|
'' |
|
6326
|
|
|
) + |
|
6327
|
|
|
(skinCss ? |
|
6328
|
|
|
' style="' + skinCss + '"' : |
|
6329
|
|
|
'' |
|
6330
|
|
|
) + |
|
6331
|
|
|
'>' + |
|
6332
|
|
|
'<div class="fc-content">' + |
|
6333
|
|
|
(timeText ? |
|
6334
|
|
|
'<div class="fc-time"' + |
|
6335
|
|
|
' data-start="' + htmlEscape(startTimeText) + '"' + |
|
|
|
|
|
|
6336
|
|
|
' data-full="' + htmlEscape(fullTimeText) + '"' + |
|
|
|
|
|
|
6337
|
|
|
'>' + |
|
6338
|
|
|
'<span>' + htmlEscape(timeText) + '</span>' + |
|
6339
|
|
|
'</div>' : |
|
6340
|
|
|
'' |
|
6341
|
|
|
) + |
|
6342
|
|
|
(event.title ? |
|
6343
|
|
|
'<div class="fc-title">' + |
|
6344
|
|
|
htmlEscape(event.title) + |
|
6345
|
|
|
'</div>' : |
|
6346
|
|
|
'' |
|
6347
|
|
|
) + |
|
6348
|
|
|
'</div>' + |
|
6349
|
|
|
'<div class="fc-bg"/>' + |
|
6350
|
|
|
(isResizable ? |
|
6351
|
|
|
'<div class="fc-resizer"/>' : |
|
6352
|
|
|
'' |
|
6353
|
|
|
) + |
|
6354
|
|
|
'</a>'; |
|
6355
|
|
|
}, |
|
6356
|
|
|
|
|
6357
|
|
|
|
|
6358
|
|
|
// Generates an object with CSS properties/values that should be applied to an event segment element. |
|
6359
|
|
|
// Contains important positioning-related properties that should be applied to any event element, customized or not. |
|
6360
|
|
|
generateSegPositionCss: function(seg) { |
|
6361
|
|
|
var view = this.view; |
|
6362
|
|
|
var isRTL = view.opt('isRTL'); |
|
6363
|
|
|
var shouldOverlap = view.opt('slotEventOverlap'); |
|
6364
|
|
|
var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point |
|
6365
|
|
|
var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point |
|
6366
|
|
|
var props = this.generateSegVerticalCss(seg); // get top/bottom first |
|
6367
|
|
|
var left; // amount of space from left edge, a fraction of the total width |
|
6368
|
|
|
var right; // amount of space from right edge, a fraction of the total width |
|
6369
|
|
|
|
|
6370
|
|
|
if (shouldOverlap) { |
|
6371
|
|
|
// double the width, but don't go beyond the maximum forward coordinate (1.0) |
|
6372
|
|
|
forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2); |
|
6373
|
|
|
} |
|
6374
|
|
|
|
|
6375
|
|
|
if (isRTL) { |
|
6376
|
|
|
left = 1 - forwardCoord; |
|
6377
|
|
|
right = backwardCoord; |
|
6378
|
|
|
} |
|
6379
|
|
|
else { |
|
6380
|
|
|
left = backwardCoord; |
|
6381
|
|
|
right = 1 - forwardCoord; |
|
6382
|
|
|
} |
|
6383
|
|
|
|
|
6384
|
|
|
props.zIndex = seg.level + 1; // convert from 0-base to 1-based |
|
6385
|
|
|
props.left = left * 100 + '%'; |
|
6386
|
|
|
props.right = right * 100 + '%'; |
|
6387
|
|
|
|
|
6388
|
|
|
if (shouldOverlap && seg.forwardPressure) { |
|
6389
|
|
|
// add padding to the edge so that forward stacked events don't cover the resizer's icon |
|
6390
|
|
|
props[isRTL ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width |
|
6391
|
|
|
} |
|
6392
|
|
|
|
|
6393
|
|
|
return props; |
|
6394
|
|
|
}, |
|
6395
|
|
|
|
|
6396
|
|
|
|
|
6397
|
|
|
// Generates an object with CSS properties for the top/bottom coordinates of a segment element |
|
6398
|
|
|
generateSegVerticalCss: function(seg) { |
|
6399
|
|
|
return { |
|
6400
|
|
|
top: seg.top, |
|
6401
|
|
|
bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container |
|
6402
|
|
|
}; |
|
6403
|
|
|
}, |
|
6404
|
|
|
|
|
6405
|
|
|
|
|
6406
|
|
|
// Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col |
|
6407
|
|
|
groupSegCols: function(segs) { |
|
6408
|
|
|
var view = this.view; |
|
6409
|
|
|
var segCols = []; |
|
6410
|
|
|
var i; |
|
6411
|
|
|
|
|
6412
|
|
|
for (i = 0; i < view.colCnt; i++) { |
|
6413
|
|
|
segCols.push([]); |
|
6414
|
|
|
} |
|
6415
|
|
|
|
|
6416
|
|
|
for (i = 0; i < segs.length; i++) { |
|
6417
|
|
|
segCols[segs[i].col].push(segs[i]); |
|
6418
|
|
|
} |
|
6419
|
|
|
|
|
6420
|
|
|
return segCols; |
|
6421
|
|
|
} |
|
6422
|
|
|
|
|
6423
|
|
|
}); |
|
6424
|
|
|
|
|
6425
|
|
|
|
|
6426
|
|
|
// Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each. |
|
6427
|
|
|
// Also reorders the given array by date! |
|
6428
|
|
|
function placeSlotSegs(segs) { |
|
6429
|
|
|
var levels; |
|
6430
|
|
|
var level0; |
|
6431
|
|
|
var i; |
|
6432
|
|
|
|
|
6433
|
|
|
segs.sort(compareSegs); // order by date |
|
6434
|
|
|
levels = buildSlotSegLevels(segs); |
|
6435
|
|
|
computeForwardSlotSegs(levels); |
|
6436
|
|
|
|
|
6437
|
|
|
if ((level0 = levels[0])) { |
|
6438
|
|
|
|
|
6439
|
|
|
for (i = 0; i < level0.length; i++) { |
|
6440
|
|
|
computeSlotSegPressures(level0[i]); |
|
6441
|
|
|
} |
|
6442
|
|
|
|
|
6443
|
|
|
for (i = 0; i < level0.length; i++) { |
|
6444
|
|
|
computeSlotSegCoords(level0[i], 0, 0); |
|
6445
|
|
|
} |
|
6446
|
|
|
} |
|
6447
|
|
|
} |
|
6448
|
|
|
|
|
6449
|
|
|
|
|
6450
|
|
|
// Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is |
|
6451
|
|
|
// left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date. |
|
6452
|
|
|
function buildSlotSegLevels(segs) { |
|
6453
|
|
|
var levels = []; |
|
6454
|
|
|
var i, seg; |
|
6455
|
|
|
var j; |
|
6456
|
|
|
|
|
6457
|
|
|
for (i=0; i<segs.length; i++) { |
|
6458
|
|
|
seg = segs[i]; |
|
6459
|
|
|
|
|
6460
|
|
|
// go through all the levels and stop on the first level where there are no collisions |
|
6461
|
|
|
for (j=0; j<levels.length; j++) { |
|
6462
|
|
|
if (!computeSlotSegCollisions(seg, levels[j]).length) { |
|
6463
|
|
|
break; |
|
6464
|
|
|
} |
|
6465
|
|
|
} |
|
6466
|
|
|
|
|
6467
|
|
|
seg.level = j; |
|
6468
|
|
|
|
|
6469
|
|
|
(levels[j] || (levels[j] = [])).push(seg); |
|
6470
|
|
|
} |
|
6471
|
|
|
|
|
6472
|
|
|
return levels; |
|
6473
|
|
|
} |
|
6474
|
|
|
|
|
6475
|
|
|
|
|
6476
|
|
|
// For every segment, figure out the other segments that are in subsequent |
|
6477
|
|
|
// levels that also occupy the same vertical space. Accumulate in seg.forwardSegs |
|
6478
|
|
|
function computeForwardSlotSegs(levels) { |
|
6479
|
|
|
var i, level; |
|
6480
|
|
|
var j, seg; |
|
6481
|
|
|
var k; |
|
6482
|
|
|
|
|
6483
|
|
|
for (i=0; i<levels.length; i++) { |
|
6484
|
|
|
level = levels[i]; |
|
6485
|
|
|
|
|
6486
|
|
|
for (j=0; j<level.length; j++) { |
|
6487
|
|
|
seg = level[j]; |
|
6488
|
|
|
|
|
6489
|
|
|
seg.forwardSegs = []; |
|
6490
|
|
|
for (k=i+1; k<levels.length; k++) { |
|
6491
|
|
|
computeSlotSegCollisions(seg, levels[k], seg.forwardSegs); |
|
6492
|
|
|
} |
|
6493
|
|
|
} |
|
6494
|
|
|
} |
|
6495
|
|
|
} |
|
6496
|
|
|
|
|
6497
|
|
|
|
|
6498
|
|
|
// Figure out which path forward (via seg.forwardSegs) results in the longest path until |
|
6499
|
|
|
// the furthest edge is reached. The number of segments in this path will be seg.forwardPressure |
|
6500
|
|
|
function computeSlotSegPressures(seg) { |
|
6501
|
|
|
var forwardSegs = seg.forwardSegs; |
|
6502
|
|
|
var forwardPressure = 0; |
|
6503
|
|
|
var i, forwardSeg; |
|
6504
|
|
|
|
|
6505
|
|
|
if (seg.forwardPressure === undefined) { // not already computed |
|
6506
|
|
|
|
|
6507
|
|
|
for (i=0; i<forwardSegs.length; i++) { |
|
6508
|
|
|
forwardSeg = forwardSegs[i]; |
|
6509
|
|
|
|
|
6510
|
|
|
// figure out the child's maximum forward path |
|
6511
|
|
|
computeSlotSegPressures(forwardSeg); |
|
6512
|
|
|
|
|
6513
|
|
|
// either use the existing maximum, or use the child's forward pressure |
|
6514
|
|
|
// plus one (for the forwardSeg itself) |
|
6515
|
|
|
forwardPressure = Math.max( |
|
6516
|
|
|
forwardPressure, |
|
6517
|
|
|
1 + forwardSeg.forwardPressure |
|
6518
|
|
|
); |
|
6519
|
|
|
} |
|
6520
|
|
|
|
|
6521
|
|
|
seg.forwardPressure = forwardPressure; |
|
6522
|
|
|
} |
|
6523
|
|
|
} |
|
6524
|
|
|
|
|
6525
|
|
|
|
|
6526
|
|
|
// Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range |
|
6527
|
|
|
// from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and |
|
6528
|
|
|
// seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left. |
|
6529
|
|
|
// |
|
6530
|
|
|
// The segment might be part of a "series", which means consecutive segments with the same pressure |
|
6531
|
|
|
// who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of |
|
6532
|
|
|
// segments behind this one in the current series, and `seriesBackwardCoord` is the starting |
|
6533
|
|
|
// coordinate of the first segment in the series. |
|
6534
|
|
|
function computeSlotSegCoords(seg, seriesBackwardPressure, seriesBackwardCoord) { |
|
6535
|
|
|
var forwardSegs = seg.forwardSegs; |
|
6536
|
|
|
var i; |
|
6537
|
|
|
|
|
6538
|
|
|
if (seg.forwardCoord === undefined) { // not already computed |
|
6539
|
|
|
|
|
6540
|
|
|
if (!forwardSegs.length) { |
|
6541
|
|
|
|
|
6542
|
|
|
// if there are no forward segments, this segment should butt up against the edge |
|
6543
|
|
|
seg.forwardCoord = 1; |
|
6544
|
|
|
} |
|
6545
|
|
|
else { |
|
6546
|
|
|
|
|
6547
|
|
|
// sort highest pressure first |
|
6548
|
|
|
forwardSegs.sort(compareForwardSlotSegs); |
|
6549
|
|
|
|
|
6550
|
|
|
// this segment's forwardCoord will be calculated from the backwardCoord of the |
|
6551
|
|
|
// highest-pressure forward segment. |
|
6552
|
|
|
computeSlotSegCoords(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord); |
|
6553
|
|
|
seg.forwardCoord = forwardSegs[0].backwardCoord; |
|
6554
|
|
|
} |
|
6555
|
|
|
|
|
6556
|
|
|
// calculate the backwardCoord from the forwardCoord. consider the series |
|
6557
|
|
|
seg.backwardCoord = seg.forwardCoord - |
|
6558
|
|
|
(seg.forwardCoord - seriesBackwardCoord) / // available width for series |
|
6559
|
|
|
(seriesBackwardPressure + 1); // # of segments in the series |
|
6560
|
|
|
|
|
6561
|
|
|
// use this segment's coordinates to computed the coordinates of the less-pressurized |
|
6562
|
|
|
// forward segments |
|
6563
|
|
|
for (i=0; i<forwardSegs.length; i++) { |
|
6564
|
|
|
computeSlotSegCoords(forwardSegs[i], 0, seg.forwardCoord); |
|
6565
|
|
|
} |
|
6566
|
|
|
} |
|
6567
|
|
|
} |
|
6568
|
|
|
|
|
6569
|
|
|
|
|
6570
|
|
|
// Find all the segments in `otherSegs` that vertically collide with `seg`. |
|
6571
|
|
|
// Append into an optionally-supplied `results` array and return. |
|
6572
|
|
|
function computeSlotSegCollisions(seg, otherSegs, results) { |
|
6573
|
|
|
results = results || []; |
|
6574
|
|
|
|
|
6575
|
|
|
for (var i=0; i<otherSegs.length; i++) { |
|
6576
|
|
|
if (isSlotSegCollision(seg, otherSegs[i])) { |
|
6577
|
|
|
results.push(otherSegs[i]); |
|
6578
|
|
|
} |
|
6579
|
|
|
} |
|
6580
|
|
|
|
|
6581
|
|
|
return results; |
|
6582
|
|
|
} |
|
6583
|
|
|
|
|
6584
|
|
|
|
|
6585
|
|
|
// Do these segments occupy the same vertical space? |
|
6586
|
|
|
function isSlotSegCollision(seg1, seg2) { |
|
6587
|
|
|
return seg1.bottom > seg2.top && seg1.top < seg2.bottom; |
|
6588
|
|
|
} |
|
6589
|
|
|
|
|
6590
|
|
|
|
|
6591
|
|
|
// A cmp function for determining which forward segment to rely on more when computing coordinates. |
|
6592
|
|
|
function compareForwardSlotSegs(seg1, seg2) { |
|
6593
|
|
|
// put higher-pressure first |
|
6594
|
|
|
return seg2.forwardPressure - seg1.forwardPressure || |
|
6595
|
|
|
// put segments that are closer to initial edge first (and favor ones with no coords yet) |
|
6596
|
|
|
(seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) || |
|
6597
|
|
|
// do normal sorting... |
|
6598
|
|
|
compareSegs(seg1, seg2); |
|
6599
|
|
|
} |
|
6600
|
|
|
|
|
6601
|
|
|
;; |
|
6602
|
|
|
|
|
6603
|
|
|
/* An abstract class from which other views inherit from |
|
6604
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
|
6605
|
|
|
// Newer methods should be written as prototype methods, not in the monster `View` function at the bottom. |
|
6606
|
|
|
|
|
6607
|
|
|
View.prototype = { |
|
6608
|
|
|
|
|
6609
|
|
|
calendar: null, // owner Calendar object |
|
6610
|
|
|
coordMap: null, // a CoordMap object for converting pixel regions to dates |
|
6611
|
|
|
el: null, // the view's containing element. set by Calendar |
|
6612
|
|
|
|
|
6613
|
|
|
// important Moments |
|
6614
|
|
|
start: null, // the date of the very first cell |
|
6615
|
|
|
end: null, // the date after the very last cell |
|
6616
|
|
|
intervalStart: null, // the start of the interval of time the view represents (1st of month for month view) |
|
6617
|
|
|
intervalEnd: null, // the exclusive end of the interval of time the view represents |
|
6618
|
|
|
|
|
6619
|
|
|
// used for cell-to-date and date-to-cell calculations |
|
6620
|
|
|
rowCnt: null, // # of weeks |
|
6621
|
|
|
colCnt: null, // # of days displayed in a week |
|
6622
|
|
|
|
|
6623
|
|
|
isSelected: false, // boolean whether cells are user-selected or not |
|
6624
|
|
|
|
|
6625
|
|
|
// subclasses can optionally use a scroll container |
|
6626
|
|
|
scrollerEl: null, // the element that will most likely scroll when content is too tall |
|
6627
|
|
|
scrollTop: null, // cached vertical scroll value |
|
6628
|
|
|
|
|
6629
|
|
|
// classNames styled by jqui themes |
|
6630
|
|
|
widgetHeaderClass: null, |
|
6631
|
|
|
widgetContentClass: null, |
|
6632
|
|
|
highlightStateClass: null, |
|
6633
|
|
|
|
|
6634
|
|
|
// document handlers, bound to `this` object |
|
6635
|
|
|
documentMousedownProxy: null, |
|
6636
|
|
|
documentDragStartProxy: null, |
|
6637
|
|
|
|
|
6638
|
|
|
|
|
6639
|
|
|
// Serves as a "constructor" to suppliment the monster `View` constructor below |
|
6640
|
|
|
init: function() { |
|
6641
|
|
|
var tm = this.opt('theme') ? 'ui' : 'fc'; |
|
6642
|
|
|
|
|
6643
|
|
|
this.widgetHeaderClass = tm + '-widget-header'; |
|
6644
|
|
|
this.widgetContentClass = tm + '-widget-content'; |
|
6645
|
|
|
this.highlightStateClass = tm + '-state-highlight'; |
|
6646
|
|
|
|
|
6647
|
|
|
// save references to `this`-bound handlers |
|
6648
|
|
|
this.documentMousedownProxy = $.proxy(this, 'documentMousedown'); |
|
6649
|
|
|
this.documentDragStartProxy = $.proxy(this, 'documentDragStart'); |
|
6650
|
|
|
}, |
|
6651
|
|
|
|
|
6652
|
|
|
|
|
6653
|
|
|
// Renders the view inside an already-defined `this.el`. |
|
6654
|
|
|
// Subclasses should override this and then call the super method afterwards. |
|
6655
|
|
|
render: function() { |
|
6656
|
|
|
this.updateSize(); |
|
6657
|
|
|
this.trigger('viewRender', this, this, this.el); |
|
6658
|
|
|
|
|
6659
|
|
|
// attach handlers to document. do it here to allow for destroy/rerender |
|
6660
|
|
|
$(document) |
|
6661
|
|
|
.on('mousedown', this.documentMousedownProxy) |
|
6662
|
|
|
.on('dragstart', this.documentDragStartProxy); // jqui drag |
|
6663
|
|
|
}, |
|
6664
|
|
|
|
|
6665
|
|
|
|
|
6666
|
|
|
// Clears all view rendering, event elements, and unregisters handlers |
|
6667
|
|
|
destroy: function() { |
|
6668
|
|
|
this.unselect(); |
|
6669
|
|
|
this.trigger('viewDestroy', this, this, this.el); |
|
6670
|
|
|
this.destroyEvents(); |
|
6671
|
|
|
this.el.empty(); // removes inner contents but leaves the element intact |
|
6672
|
|
|
|
|
6673
|
|
|
$(document) |
|
6674
|
|
|
.off('mousedown', this.documentMousedownProxy) |
|
6675
|
|
|
.off('dragstart', this.documentDragStartProxy); |
|
6676
|
|
|
}, |
|
6677
|
|
|
|
|
6678
|
|
|
|
|
6679
|
|
|
// Used to determine what happens when the users clicks next/prev. Given -1 for prev, 1 for next. |
|
6680
|
|
|
// Should apply the delta to `date` (a Moment) and return it. |
|
6681
|
|
|
incrementDate: function(date, delta) { |
|
6682
|
|
|
// subclasses should implement |
|
6683
|
|
|
}, |
|
6684
|
|
|
|
|
6685
|
|
|
|
|
6686
|
|
|
/* Dimensions |
|
6687
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
6688
|
|
|
|
|
6689
|
|
|
|
|
6690
|
|
|
// Refreshes anything dependant upon sizing of the container element of the grid |
|
6691
|
|
|
updateSize: function(isResize) { |
|
6692
|
|
|
if (isResize) { |
|
6693
|
|
|
this.recordScroll(); |
|
6694
|
|
|
} |
|
6695
|
|
|
this.updateHeight(); |
|
6696
|
|
|
this.updateWidth(); |
|
6697
|
|
|
}, |
|
6698
|
|
|
|
|
6699
|
|
|
|
|
6700
|
|
|
// Refreshes the horizontal dimensions of the calendar |
|
6701
|
|
|
updateWidth: function() { |
|
6702
|
|
|
// subclasses should implement |
|
6703
|
|
|
}, |
|
6704
|
|
|
|
|
6705
|
|
|
|
|
6706
|
|
|
// Refreshes the vertical dimensions of the calendar |
|
6707
|
|
|
updateHeight: function() { |
|
6708
|
|
|
var calendar = this.calendar; // we poll the calendar for height information |
|
6709
|
|
|
|
|
6710
|
|
|
this.setHeight( |
|
6711
|
|
|
calendar.getSuggestedViewHeight(), |
|
6712
|
|
|
calendar.isHeightAuto() |
|
6713
|
|
|
); |
|
6714
|
|
|
}, |
|
6715
|
|
|
|
|
6716
|
|
|
|
|
6717
|
|
|
// Updates the vertical dimensions of the calendar to the specified height. |
|
6718
|
|
|
// if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height. |
|
6719
|
|
|
setHeight: function(height, isAuto) { |
|
6720
|
|
|
// subclasses should implement |
|
6721
|
|
|
}, |
|
6722
|
|
|
|
|
6723
|
|
|
|
|
6724
|
|
|
// Given the total height of the view, return the number of pixels that should be used for the scroller. |
|
6725
|
|
|
// Utility for subclasses. |
|
6726
|
|
|
computeScrollerHeight: function(totalHeight) { |
|
6727
|
|
|
var both = this.el.add(this.scrollerEl); |
|
6728
|
|
|
var otherHeight; // cumulative height of everything that is not the scrollerEl in the view (header+borders) |
|
6729
|
|
|
|
|
6730
|
|
|
// fuckin IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked |
|
6731
|
|
|
both.css({ |
|
6732
|
|
|
position: 'relative', // cause a reflow, which will force fresh dimension recalculation |
|
6733
|
|
|
left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll |
|
6734
|
|
|
}); |
|
6735
|
|
|
otherHeight = this.el.outerHeight() - this.scrollerEl.height(); // grab the dimensions |
|
6736
|
|
|
both.css({ position: '', left: '' }); // undo hack |
|
6737
|
|
|
|
|
6738
|
|
|
return totalHeight - otherHeight; |
|
6739
|
|
|
}, |
|
6740
|
|
|
|
|
6741
|
|
|
|
|
6742
|
|
|
// Called for remembering the current scroll value of the scroller. |
|
6743
|
|
|
// Should be called before there is a destructive operation (like removing DOM elements) that might inadvertently |
|
6744
|
|
|
// change the scroll of the container. |
|
6745
|
|
|
recordScroll: function() { |
|
6746
|
|
|
if (this.scrollerEl) { |
|
6747
|
|
|
this.scrollTop = this.scrollerEl.scrollTop(); |
|
6748
|
|
|
} |
|
6749
|
|
|
}, |
|
6750
|
|
|
|
|
6751
|
|
|
|
|
6752
|
|
|
// Set the scroll value of the scroller to the previously recorded value. |
|
6753
|
|
|
// Should be called after we know the view's dimensions have been restored following some type of destructive |
|
6754
|
|
|
// operation (like temporarily removing DOM elements). |
|
6755
|
|
|
restoreScroll: function() { |
|
6756
|
|
|
if (this.scrollTop !== null) { |
|
6757
|
|
|
this.scrollerEl.scrollTop(this.scrollTop); |
|
6758
|
|
|
} |
|
6759
|
|
|
}, |
|
6760
|
|
|
|
|
6761
|
|
|
|
|
6762
|
|
|
/* Events |
|
6763
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
6764
|
|
|
|
|
6765
|
|
|
|
|
6766
|
|
|
// Renders the events onto the view. |
|
6767
|
|
|
// Should be overriden by subclasses. Subclasses should call the super-method afterwards. |
|
6768
|
|
|
renderEvents: function(events) { |
|
6769
|
|
|
this.segEach(function(seg) { |
|
6770
|
|
|
this.trigger('eventAfterRender', seg.event, seg.event, seg.el); |
|
6771
|
|
|
}); |
|
6772
|
|
|
this.trigger('eventAfterAllRender'); |
|
6773
|
|
|
}, |
|
6774
|
|
|
|
|
6775
|
|
|
|
|
6776
|
|
|
// Removes event elements from the view. |
|
6777
|
|
|
// Should be overridden by subclasses. Should call this super-method FIRST, then subclass DOM destruction. |
|
6778
|
|
|
destroyEvents: function() { |
|
6779
|
|
|
this.segEach(function(seg) { |
|
6780
|
|
|
this.trigger('eventDestroy', seg.event, seg.event, seg.el); |
|
6781
|
|
|
}); |
|
6782
|
|
|
}, |
|
6783
|
|
|
|
|
6784
|
|
|
|
|
6785
|
|
|
// Given an event and the default element used for rendering, returns the element that should actually be used. |
|
6786
|
|
|
// Basically runs events and elements through the eventRender hook. |
|
6787
|
|
|
resolveEventEl: function(event, el) { |
|
6788
|
|
|
var custom = this.trigger('eventRender', event, event, el); |
|
6789
|
|
|
|
|
6790
|
|
|
if (custom === false) { // means don't render at all |
|
6791
|
|
|
el = null; |
|
6792
|
|
|
} |
|
6793
|
|
|
else if (custom && custom !== true) { |
|
6794
|
|
|
el = $(custom); |
|
6795
|
|
|
} |
|
6796
|
|
|
|
|
6797
|
|
|
return el; |
|
6798
|
|
|
}, |
|
6799
|
|
|
|
|
6800
|
|
|
|
|
6801
|
|
|
// Hides all rendered event segments linked to the given event |
|
6802
|
|
|
showEvent: function(event) { |
|
6803
|
|
|
this.segEach(function(seg) { |
|
6804
|
|
|
seg.el.css('visibility', ''); |
|
6805
|
|
|
}, event); |
|
6806
|
|
|
}, |
|
6807
|
|
|
|
|
6808
|
|
|
|
|
6809
|
|
|
// Shows all rendered event segments linked to the given event |
|
6810
|
|
|
hideEvent: function(event) { |
|
6811
|
|
|
this.segEach(function(seg) { |
|
6812
|
|
|
seg.el.css('visibility', 'hidden'); |
|
6813
|
|
|
}, event); |
|
6814
|
|
|
}, |
|
6815
|
|
|
|
|
6816
|
|
|
|
|
6817
|
|
|
// Iterates through event segments. Goes through all by default. |
|
6818
|
|
|
// If the optional `event` argument is specified, only iterates through segments linked to that event. |
|
6819
|
|
|
// The `this` value of the callback function will be the view. |
|
6820
|
|
|
segEach: function(func, event) { |
|
6821
|
|
|
var segs = this.getSegs(); |
|
6822
|
|
|
var i; |
|
6823
|
|
|
|
|
6824
|
|
|
for (i = 0; i < segs.length; i++) { |
|
6825
|
|
|
if (!event || segs[i].event._id === event._id) { |
|
6826
|
|
|
func.call(this, segs[i]); |
|
6827
|
|
|
} |
|
6828
|
|
|
} |
|
6829
|
|
|
}, |
|
6830
|
|
|
|
|
6831
|
|
|
|
|
6832
|
|
|
// Retrieves all the rendered segment objects for the view |
|
6833
|
|
|
getSegs: function() { |
|
6834
|
|
|
// subclasses must implement |
|
6835
|
|
|
}, |
|
6836
|
|
|
|
|
6837
|
|
|
|
|
6838
|
|
|
/* Event Drag Visualization |
|
6839
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
6840
|
|
|
|
|
6841
|
|
|
|
|
6842
|
|
|
// Renders a visual indication of an event hovering over the specified date. |
|
6843
|
|
|
// `end` is a Moment and might be null. |
|
6844
|
|
|
// `seg` might be null. if specified, it is the segment object of the event being dragged. |
|
6845
|
|
|
// otherwise, an external event from outside the calendar is being dragged. |
|
6846
|
|
|
renderDrag: function(start, end, seg) { |
|
6847
|
|
|
// subclasses should implement |
|
6848
|
|
|
}, |
|
6849
|
|
|
|
|
6850
|
|
|
|
|
6851
|
|
|
// Unrenders a visual indication of event hovering |
|
6852
|
|
|
destroyDrag: function() { |
|
6853
|
|
|
// subclasses should implement |
|
6854
|
|
|
}, |
|
6855
|
|
|
|
|
6856
|
|
|
|
|
6857
|
|
|
// Handler for accepting externally dragged events being dropped in the view. |
|
6858
|
|
|
// Gets called when jqui's 'dragstart' is fired. |
|
6859
|
|
|
documentDragStart: function(ev, ui) { |
|
6860
|
|
|
var _this = this; |
|
6861
|
|
|
var dropDate = null; |
|
6862
|
|
|
var dragListener; |
|
6863
|
|
|
|
|
6864
|
|
|
if (this.opt('droppable')) { // only listen if this setting is on |
|
6865
|
|
|
|
|
6866
|
|
|
// listener that tracks mouse movement over date-associated pixel regions |
|
6867
|
|
|
dragListener = new DragListener(this.coordMap, { |
|
6868
|
|
|
cellOver: function(cell, date) { |
|
6869
|
|
|
dropDate = date; |
|
6870
|
|
|
_this.renderDrag(date); |
|
6871
|
|
|
}, |
|
6872
|
|
|
cellOut: function() { |
|
6873
|
|
|
dropDate = null; |
|
6874
|
|
|
_this.destroyDrag(); |
|
6875
|
|
|
} |
|
6876
|
|
|
}); |
|
6877
|
|
|
|
|
6878
|
|
|
// gets called, only once, when jqui drag is finished |
|
6879
|
|
|
$(document).one('dragstop', function(ev, ui) { |
|
6880
|
|
|
_this.destroyDrag(); |
|
6881
|
|
|
if (dropDate) { |
|
6882
|
|
|
_this.trigger('drop', ev.target, dropDate, ev, ui); |
|
6883
|
|
|
} |
|
6884
|
|
|
}); |
|
6885
|
|
|
|
|
6886
|
|
|
dragListener.startDrag(ev); // start listening immediately |
|
6887
|
|
|
} |
|
6888
|
|
|
}, |
|
6889
|
|
|
|
|
6890
|
|
|
|
|
6891
|
|
|
/* Selection |
|
6892
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
6893
|
|
|
|
|
6894
|
|
|
|
|
6895
|
|
|
// Selects a date range on the view. `start` and `end` are both Moments. |
|
6896
|
|
|
// `ev` is the native mouse event that begin the interaction. |
|
6897
|
|
|
select: function(start, end, ev) { |
|
6898
|
|
|
this.unselect(ev); |
|
6899
|
|
|
this.renderSelection(start, end); |
|
6900
|
|
|
this.reportSelection(start, end, ev); |
|
6901
|
|
|
}, |
|
6902
|
|
|
|
|
6903
|
|
|
|
|
6904
|
|
|
// Renders a visual indication of the selection |
|
6905
|
|
|
renderSelection: function(start, end) { |
|
6906
|
|
|
// subclasses should implement |
|
6907
|
|
|
}, |
|
6908
|
|
|
|
|
6909
|
|
|
|
|
6910
|
|
|
// Called when a new selection is made. Updates internal state and triggers handlers. |
|
6911
|
|
|
reportSelection: function(start, end, ev) { |
|
6912
|
|
|
this.isSelected = true; |
|
6913
|
|
|
this.trigger('select', null, start, end, ev); |
|
6914
|
|
|
}, |
|
6915
|
|
|
|
|
6916
|
|
|
|
|
6917
|
|
|
// Undoes a selection. updates in the internal state and triggers handlers. |
|
6918
|
|
|
// `ev` is the native mouse event that began the interaction. |
|
6919
|
|
|
unselect: function(ev) { |
|
6920
|
|
|
if (this.isSelected) { |
|
6921
|
|
|
this.isSelected = false; |
|
6922
|
|
|
this.destroySelection(); |
|
6923
|
|
|
this.trigger('unselect', null, ev); |
|
6924
|
|
|
} |
|
6925
|
|
|
}, |
|
6926
|
|
|
|
|
6927
|
|
|
|
|
6928
|
|
|
// Unrenders a visual indication of selection |
|
6929
|
|
|
destroySelection: function() { |
|
6930
|
|
|
// subclasses should implement |
|
6931
|
|
|
}, |
|
6932
|
|
|
|
|
6933
|
|
|
|
|
6934
|
|
|
// Handler for unselecting when the user clicks something and the 'unselectAuto' setting is on |
|
6935
|
|
|
documentMousedown: function(ev) { |
|
6936
|
|
|
var ignore; |
|
6937
|
|
|
|
|
6938
|
|
|
// is there a selection, and has the user made a proper left click? |
|
6939
|
|
|
if (this.isSelected && this.opt('unselectAuto') && isPrimaryMouseButton(ev)) { |
|
6940
|
|
|
|
|
6941
|
|
|
// only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element |
|
6942
|
|
|
ignore = this.opt('unselectCancel'); |
|
6943
|
|
|
if (!ignore || !$(ev.target).closest(ignore).length) { |
|
6944
|
|
|
this.unselect(ev); |
|
6945
|
|
|
} |
|
6946
|
|
|
} |
|
6947
|
|
|
} |
|
6948
|
|
|
|
|
6949
|
|
|
}; |
|
6950
|
|
|
|
|
6951
|
|
|
|
|
6952
|
|
|
// We are mixing JavaScript OOP design patterns here by putting methods and member variables in the closed scope of the |
|
6953
|
|
|
// constructor. Going forward, methods should be part of the prototype. |
|
6954
|
|
|
function View(calendar) { |
|
6955
|
|
|
var t = this; |
|
6956
|
|
|
|
|
6957
|
|
|
// exports |
|
6958
|
|
|
t.calendar = calendar; |
|
6959
|
|
|
t.opt = opt; |
|
6960
|
|
|
t.trigger = trigger; |
|
6961
|
|
|
t.isEventDraggable = isEventDraggable; |
|
6962
|
|
|
t.isEventResizable = isEventResizable; |
|
6963
|
|
|
t.eventDrop = eventDrop; |
|
6964
|
|
|
t.eventResize = eventResize; |
|
6965
|
|
|
|
|
6966
|
|
|
// imports |
|
6967
|
|
|
var reportEventChange = calendar.reportEventChange; |
|
6968
|
|
|
|
|
6969
|
|
|
// locals |
|
6970
|
|
|
var options = calendar.options; |
|
6971
|
|
|
var nextDayThreshold = moment.duration(options.nextDayThreshold); |
|
6972
|
|
|
|
|
6973
|
|
|
|
|
6974
|
|
|
t.init(); // the "constructor" that concerns the prototype methods |
|
6975
|
|
|
|
|
6976
|
|
|
|
|
6977
|
|
|
function opt(name) { |
|
6978
|
|
|
var v = options[name]; |
|
6979
|
|
|
if ($.isPlainObject(v) && !isForcedAtomicOption(name)) { |
|
6980
|
|
|
return smartProperty(v, t.name); |
|
6981
|
|
|
} |
|
6982
|
|
|
return v; |
|
6983
|
|
|
} |
|
6984
|
|
|
|
|
6985
|
|
|
|
|
6986
|
|
|
function trigger(name, thisObj) { |
|
6987
|
|
|
return calendar.trigger.apply( |
|
6988
|
|
|
calendar, |
|
6989
|
|
|
[name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t]) |
|
6990
|
|
|
); |
|
6991
|
|
|
} |
|
6992
|
|
|
|
|
6993
|
|
|
|
|
6994
|
|
|
|
|
6995
|
|
|
/* Event Editable Boolean Calculations |
|
6996
|
|
|
------------------------------------------------------------------------------*/ |
|
6997
|
|
|
|
|
6998
|
|
|
|
|
6999
|
|
|
function isEventDraggable(event) { |
|
7000
|
|
|
var source = event.source || {}; |
|
7001
|
|
|
|
|
7002
|
|
|
return firstDefined( |
|
7003
|
|
|
event.startEditable, |
|
7004
|
|
|
source.startEditable, |
|
7005
|
|
|
opt('eventStartEditable'), |
|
7006
|
|
|
event.editable, |
|
7007
|
|
|
source.editable, |
|
7008
|
|
|
opt('editable') |
|
7009
|
|
|
); |
|
7010
|
|
|
} |
|
7011
|
|
|
|
|
7012
|
|
|
|
|
7013
|
|
|
function isEventResizable(event) { |
|
7014
|
|
|
var source = event.source || {}; |
|
7015
|
|
|
|
|
7016
|
|
|
return firstDefined( |
|
7017
|
|
|
event.durationEditable, |
|
7018
|
|
|
source.durationEditable, |
|
7019
|
|
|
opt('eventDurationEditable'), |
|
7020
|
|
|
event.editable, |
|
7021
|
|
|
source.editable, |
|
7022
|
|
|
opt('editable') |
|
7023
|
|
|
); |
|
7024
|
|
|
} |
|
7025
|
|
|
|
|
7026
|
|
|
|
|
7027
|
|
|
|
|
7028
|
|
|
/* Event Elements |
|
7029
|
|
|
------------------------------------------------------------------------------*/ |
|
7030
|
|
|
|
|
7031
|
|
|
|
|
7032
|
|
|
// Compute the text that should be displayed on an event's element. |
|
7033
|
|
|
// Based off the settings of the view. Possible signatures: |
|
7034
|
|
|
// .getEventTimeText(event, formatStr) |
|
7035
|
|
|
// .getEventTimeText(startMoment, endMoment, formatStr) |
|
7036
|
|
|
// .getEventTimeText(startMoment, null, formatStr) |
|
7037
|
|
|
// `timeFormat` is used but the `formatStr` argument can be used to override. |
|
7038
|
|
|
t.getEventTimeText = function(event, formatStr) { |
|
7039
|
|
|
var start; |
|
7040
|
|
|
var end; |
|
7041
|
|
|
|
|
7042
|
|
|
if (typeof event === 'object' && typeof formatStr === 'object') { |
|
7043
|
|
|
// first two arguments are actually moments (or null). shift arguments. |
|
7044
|
|
|
start = event; |
|
7045
|
|
|
end = formatStr; |
|
7046
|
|
|
formatStr = arguments[2]; |
|
7047
|
|
|
} |
|
7048
|
|
|
else { |
|
7049
|
|
|
// otherwise, an event object was the first argument |
|
7050
|
|
|
start = event.start; |
|
7051
|
|
|
end = event.end; |
|
7052
|
|
|
} |
|
7053
|
|
|
|
|
7054
|
|
|
formatStr = formatStr || opt('timeFormat'); |
|
7055
|
|
|
|
|
7056
|
|
|
if (end && opt('displayEventEnd')) { |
|
7057
|
|
|
return calendar.formatRange(start, end, formatStr); |
|
7058
|
|
|
} |
|
7059
|
|
|
else { |
|
7060
|
|
|
return calendar.formatDate(start, formatStr); |
|
7061
|
|
|
} |
|
7062
|
|
|
}; |
|
7063
|
|
|
|
|
7064
|
|
|
|
|
7065
|
|
|
|
|
7066
|
|
|
/* Event Modification Reporting |
|
7067
|
|
|
---------------------------------------------------------------------------------*/ |
|
7068
|
|
|
|
|
7069
|
|
|
|
|
7070
|
|
|
function eventDrop(el, event, newStart, ev) { |
|
7071
|
|
|
var mutateResult = calendar.mutateEvent(event, newStart, null); |
|
7072
|
|
|
|
|
7073
|
|
|
trigger( |
|
7074
|
|
|
'eventDrop', |
|
7075
|
|
|
el, |
|
7076
|
|
|
event, |
|
7077
|
|
|
mutateResult.dateDelta, |
|
7078
|
|
|
function() { |
|
7079
|
|
|
mutateResult.undo(); |
|
7080
|
|
|
reportEventChange(); |
|
7081
|
|
|
}, |
|
7082
|
|
|
ev, |
|
7083
|
|
|
{} // jqui dummy |
|
7084
|
|
|
); |
|
7085
|
|
|
|
|
7086
|
|
|
reportEventChange(); |
|
7087
|
|
|
} |
|
7088
|
|
|
|
|
7089
|
|
|
|
|
7090
|
|
|
function eventResize(el, event, newEnd, ev) { |
|
7091
|
|
|
var mutateResult = calendar.mutateEvent(event, null, newEnd); |
|
7092
|
|
|
|
|
7093
|
|
|
trigger( |
|
7094
|
|
|
'eventResize', |
|
7095
|
|
|
el, |
|
7096
|
|
|
event, |
|
7097
|
|
|
mutateResult.durationDelta, |
|
7098
|
|
|
function() { |
|
7099
|
|
|
mutateResult.undo(); |
|
7100
|
|
|
reportEventChange(); |
|
7101
|
|
|
}, |
|
7102
|
|
|
ev, |
|
7103
|
|
|
{} // jqui dummy |
|
7104
|
|
|
); |
|
7105
|
|
|
|
|
7106
|
|
|
reportEventChange(); |
|
7107
|
|
|
} |
|
7108
|
|
|
|
|
7109
|
|
|
|
|
7110
|
|
|
// ==================================================================================================== |
|
7111
|
|
|
// Utilities for day "cells" |
|
7112
|
|
|
// ==================================================================================================== |
|
7113
|
|
|
// The "basic" views are completely made up of day cells. |
|
7114
|
|
|
// The "agenda" views have day cells at the top "all day" slot. |
|
7115
|
|
|
// This was the obvious common place to put these utilities, but they should be abstracted out into |
|
7116
|
|
|
// a more meaningful class (like DayEventRenderer). |
|
7117
|
|
|
// ==================================================================================================== |
|
7118
|
|
|
|
|
7119
|
|
|
|
|
7120
|
|
|
// For determining how a given "cell" translates into a "date": |
|
7121
|
|
|
// |
|
7122
|
|
|
// 1. Convert the "cell" (row and column) into a "cell offset" (the # of the cell, cronologically from the first). |
|
7123
|
|
|
// Keep in mind that column indices are inverted with isRTL. This is taken into account. |
|
7124
|
|
|
// |
|
7125
|
|
|
// 2. Convert the "cell offset" to a "day offset" (the # of days since the first visible day in the view). |
|
7126
|
|
|
// |
|
7127
|
|
|
// 3. Convert the "day offset" into a "date" (a Moment). |
|
7128
|
|
|
// |
|
7129
|
|
|
// The reverse transformation happens when transforming a date into a cell. |
|
7130
|
|
|
|
|
7131
|
|
|
|
|
7132
|
|
|
// exports |
|
7133
|
|
|
t.isHiddenDay = isHiddenDay; |
|
7134
|
|
|
t.skipHiddenDays = skipHiddenDays; |
|
7135
|
|
|
t.getCellsPerWeek = getCellsPerWeek; |
|
7136
|
|
|
t.dateToCell = dateToCell; |
|
7137
|
|
|
t.dateToDayOffset = dateToDayOffset; |
|
7138
|
|
|
t.dayOffsetToCellOffset = dayOffsetToCellOffset; |
|
7139
|
|
|
t.cellOffsetToCell = cellOffsetToCell; |
|
7140
|
|
|
t.cellToDate = cellToDate; |
|
7141
|
|
|
t.cellToCellOffset = cellToCellOffset; |
|
7142
|
|
|
t.cellOffsetToDayOffset = cellOffsetToDayOffset; |
|
7143
|
|
|
t.dayOffsetToDate = dayOffsetToDate; |
|
7144
|
|
|
t.rangeToSegments = rangeToSegments; |
|
7145
|
|
|
t.isMultiDayEvent = isMultiDayEvent; |
|
7146
|
|
|
|
|
7147
|
|
|
|
|
7148
|
|
|
// internals |
|
7149
|
|
|
var hiddenDays = opt('hiddenDays') || []; // array of day-of-week indices that are hidden |
|
7150
|
|
|
var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool) |
|
7151
|
|
|
var cellsPerWeek; |
|
7152
|
|
|
var dayToCellMap = []; // hash from dayIndex -> cellIndex, for one week |
|
7153
|
|
|
var cellToDayMap = []; // hash from cellIndex -> dayIndex, for one week |
|
7154
|
|
|
var isRTL = opt('isRTL'); |
|
7155
|
|
|
|
|
7156
|
|
|
|
|
7157
|
|
|
// initialize important internal variables |
|
7158
|
|
|
(function() { |
|
7159
|
|
|
|
|
7160
|
|
|
if (opt('weekends') === false) { |
|
7161
|
|
|
hiddenDays.push(0, 6); // 0=sunday, 6=saturday |
|
7162
|
|
|
} |
|
7163
|
|
|
|
|
7164
|
|
|
// Loop through a hypothetical week and determine which |
|
7165
|
|
|
// days-of-week are hidden. Record in both hashes (one is the reverse of the other). |
|
7166
|
|
|
for (var dayIndex=0, cellIndex=0; dayIndex<7; dayIndex++) { |
|
7167
|
|
|
dayToCellMap[dayIndex] = cellIndex; |
|
7168
|
|
|
isHiddenDayHash[dayIndex] = $.inArray(dayIndex, hiddenDays) != -1; |
|
7169
|
|
|
if (!isHiddenDayHash[dayIndex]) { |
|
7170
|
|
|
cellToDayMap[cellIndex] = dayIndex; |
|
7171
|
|
|
cellIndex++; |
|
7172
|
|
|
} |
|
7173
|
|
|
} |
|
7174
|
|
|
|
|
7175
|
|
|
cellsPerWeek = cellIndex; |
|
7176
|
|
|
if (!cellsPerWeek) { |
|
7177
|
|
|
throw 'invalid hiddenDays'; // all days were hidden? bad. |
|
7178
|
|
|
} |
|
7179
|
|
|
|
|
7180
|
|
|
})(); |
|
7181
|
|
|
|
|
7182
|
|
|
|
|
7183
|
|
|
// Is the current day hidden? |
|
7184
|
|
|
// `day` is a day-of-week index (0-6), or a Moment |
|
7185
|
|
|
function isHiddenDay(day) { |
|
7186
|
|
|
if (moment.isMoment(day)) { |
|
7187
|
|
|
day = day.day(); |
|
7188
|
|
|
} |
|
7189
|
|
|
return isHiddenDayHash[day]; |
|
7190
|
|
|
} |
|
7191
|
|
|
|
|
7192
|
|
|
|
|
7193
|
|
|
function getCellsPerWeek() { |
|
7194
|
|
|
return cellsPerWeek; |
|
7195
|
|
|
} |
|
7196
|
|
|
|
|
7197
|
|
|
|
|
7198
|
|
|
// Incrementing the current day until it is no longer a hidden day, returning a copy. |
|
7199
|
|
|
// If the initial value of `date` is not a hidden day, don't do anything. |
|
7200
|
|
|
// Pass `isExclusive` as `true` if you are dealing with an end date. |
|
7201
|
|
|
// `inc` defaults to `1` (increment one day forward each time) |
|
7202
|
|
|
function skipHiddenDays(date, inc, isExclusive) { |
|
7203
|
|
|
var out = date.clone(); |
|
7204
|
|
|
inc = inc || 1; |
|
7205
|
|
|
while ( |
|
7206
|
|
|
isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7] |
|
7207
|
|
|
) { |
|
7208
|
|
|
out.add(inc, 'days'); |
|
7209
|
|
|
} |
|
7210
|
|
|
return out; |
|
7211
|
|
|
} |
|
7212
|
|
|
|
|
7213
|
|
|
|
|
7214
|
|
|
// |
|
7215
|
|
|
// TRANSFORMATIONS: cell -> cell offset -> day offset -> date |
|
7216
|
|
|
// |
|
7217
|
|
|
|
|
7218
|
|
|
// cell -> date (combines all transformations) |
|
7219
|
|
|
// Possible arguments: |
|
7220
|
|
|
// - row, col |
|
7221
|
|
|
// - { row:#, col: # } |
|
7222
|
|
|
function cellToDate() { |
|
7223
|
|
|
var cellOffset = cellToCellOffset.apply(null, arguments); |
|
7224
|
|
|
var dayOffset = cellOffsetToDayOffset(cellOffset); |
|
7225
|
|
|
var date = dayOffsetToDate(dayOffset); |
|
7226
|
|
|
return date; |
|
7227
|
|
|
} |
|
7228
|
|
|
|
|
7229
|
|
|
// cell -> cell offset |
|
7230
|
|
|
// Possible arguments: |
|
7231
|
|
|
// - row, col |
|
7232
|
|
|
// - { row:#, col:# } |
|
7233
|
|
|
function cellToCellOffset(row, col) { |
|
7234
|
|
|
var colCnt = t.colCnt; |
|
7235
|
|
|
|
|
7236
|
|
|
// rtl variables. wish we could pre-populate these. but where? |
|
7237
|
|
|
var dis = isRTL ? -1 : 1; |
|
7238
|
|
|
var dit = isRTL ? colCnt - 1 : 0; |
|
7239
|
|
|
|
|
7240
|
|
|
if (typeof row == 'object') { |
|
7241
|
|
|
col = row.col; |
|
7242
|
|
|
row = row.row; |
|
7243
|
|
|
} |
|
7244
|
|
|
var cellOffset = row * colCnt + (col * dis + dit); // column, adjusted for RTL (dis & dit) |
|
7245
|
|
|
|
|
7246
|
|
|
return cellOffset; |
|
7247
|
|
|
} |
|
7248
|
|
|
|
|
7249
|
|
|
// cell offset -> day offset |
|
7250
|
|
|
function cellOffsetToDayOffset(cellOffset) { |
|
7251
|
|
|
var day0 = t.start.day(); // first date's day of week |
|
7252
|
|
|
cellOffset += dayToCellMap[day0]; // normlize cellOffset to beginning-of-week |
|
7253
|
|
|
return Math.floor(cellOffset / cellsPerWeek) * 7 + // # of days from full weeks |
|
7254
|
|
|
cellToDayMap[ // # of days from partial last week |
|
7255
|
|
|
(cellOffset % cellsPerWeek + cellsPerWeek) % cellsPerWeek // crazy math to handle negative cellOffsets |
|
7256
|
|
|
] - |
|
7257
|
|
|
day0; // adjustment for beginning-of-week normalization |
|
7258
|
|
|
} |
|
7259
|
|
|
|
|
7260
|
|
|
// day offset -> date |
|
7261
|
|
|
function dayOffsetToDate(dayOffset) { |
|
7262
|
|
|
return t.start.clone().add(dayOffset, 'days'); |
|
7263
|
|
|
} |
|
7264
|
|
|
|
|
7265
|
|
|
|
|
7266
|
|
|
// |
|
7267
|
|
|
// TRANSFORMATIONS: date -> day offset -> cell offset -> cell |
|
7268
|
|
|
// |
|
7269
|
|
|
|
|
7270
|
|
|
// date -> cell (combines all transformations) |
|
7271
|
|
|
function dateToCell(date) { |
|
7272
|
|
|
var dayOffset = dateToDayOffset(date); |
|
7273
|
|
|
var cellOffset = dayOffsetToCellOffset(dayOffset); |
|
7274
|
|
|
var cell = cellOffsetToCell(cellOffset); |
|
7275
|
|
|
return cell; |
|
7276
|
|
|
} |
|
7277
|
|
|
|
|
7278
|
|
|
// date -> day offset |
|
7279
|
|
|
function dateToDayOffset(date) { |
|
7280
|
|
|
return date.clone().stripTime().diff(t.start, 'days'); |
|
7281
|
|
|
} |
|
7282
|
|
|
|
|
7283
|
|
|
// day offset -> cell offset |
|
7284
|
|
|
function dayOffsetToCellOffset(dayOffset) { |
|
7285
|
|
|
var day0 = t.start.day(); // first date's day of week |
|
7286
|
|
|
dayOffset += day0; // normalize dayOffset to beginning-of-week |
|
7287
|
|
|
return Math.floor(dayOffset / 7) * cellsPerWeek + // # of cells from full weeks |
|
7288
|
|
|
dayToCellMap[ // # of cells from partial last week |
|
7289
|
|
|
(dayOffset % 7 + 7) % 7 // crazy math to handle negative dayOffsets |
|
7290
|
|
|
] - |
|
7291
|
|
|
dayToCellMap[day0]; // adjustment for beginning-of-week normalization |
|
7292
|
|
|
} |
|
7293
|
|
|
|
|
7294
|
|
|
// cell offset -> cell (object with row & col keys) |
|
7295
|
|
|
function cellOffsetToCell(cellOffset) { |
|
7296
|
|
|
var colCnt = t.colCnt; |
|
7297
|
|
|
|
|
7298
|
|
|
// rtl variables. wish we could pre-populate these. but where? |
|
7299
|
|
|
var dis = isRTL ? -1 : 1; |
|
7300
|
|
|
var dit = isRTL ? colCnt - 1 : 0; |
|
7301
|
|
|
|
|
7302
|
|
|
var row = Math.floor(cellOffset / colCnt); |
|
7303
|
|
|
var col = ((cellOffset % colCnt + colCnt) % colCnt) * dis + dit; // column, adjusted for RTL (dis & dit) |
|
7304
|
|
|
return { |
|
7305
|
|
|
row: row, |
|
7306
|
|
|
col: col |
|
7307
|
|
|
}; |
|
7308
|
|
|
} |
|
7309
|
|
|
|
|
7310
|
|
|
|
|
7311
|
|
|
// |
|
7312
|
|
|
// Converts a date range into an array of segment objects. |
|
7313
|
|
|
// "Segments" are horizontal stretches of time, sliced up by row. |
|
7314
|
|
|
// A segment object has the following properties: |
|
7315
|
|
|
// - row |
|
7316
|
|
|
// - cols |
|
7317
|
|
|
// - isStart |
|
7318
|
|
|
// - isEnd |
|
7319
|
|
|
// |
|
7320
|
|
|
function rangeToSegments(start, end) { |
|
7321
|
|
|
|
|
7322
|
|
|
var rowCnt = t.rowCnt; |
|
7323
|
|
|
var colCnt = t.colCnt; |
|
7324
|
|
|
var segments = []; // array of segments to return |
|
7325
|
|
|
|
|
7326
|
|
|
// day offset for given date range |
|
7327
|
|
|
var dayRange = computeDayRange(start, end); // convert to a whole-day range |
|
7328
|
|
|
var rangeDayOffsetStart = dateToDayOffset(dayRange.start); |
|
7329
|
|
|
var rangeDayOffsetEnd = dateToDayOffset(dayRange.end); // an exclusive value |
|
7330
|
|
|
|
|
7331
|
|
|
// first and last cell offset for the given date range |
|
7332
|
|
|
// "last" implies inclusivity |
|
7333
|
|
|
var rangeCellOffsetFirst = dayOffsetToCellOffset(rangeDayOffsetStart); |
|
7334
|
|
|
var rangeCellOffsetLast = dayOffsetToCellOffset(rangeDayOffsetEnd) - 1; |
|
7335
|
|
|
|
|
7336
|
|
|
// loop through all the rows in the view |
|
7337
|
|
|
for (var row=0; row<rowCnt; row++) { |
|
7338
|
|
|
|
|
7339
|
|
|
// first and last cell offset for the row |
|
7340
|
|
|
var rowCellOffsetFirst = row * colCnt; |
|
7341
|
|
|
var rowCellOffsetLast = rowCellOffsetFirst + colCnt - 1; |
|
7342
|
|
|
|
|
7343
|
|
|
// get the segment's cell offsets by constraining the range's cell offsets to the bounds of the row |
|
7344
|
|
|
var segmentCellOffsetFirst = Math.max(rangeCellOffsetFirst, rowCellOffsetFirst); |
|
7345
|
|
|
var segmentCellOffsetLast = Math.min(rangeCellOffsetLast, rowCellOffsetLast); |
|
7346
|
|
|
|
|
7347
|
|
|
// make sure segment's offsets are valid and in view |
|
7348
|
|
|
if (segmentCellOffsetFirst <= segmentCellOffsetLast) { |
|
7349
|
|
|
|
|
7350
|
|
|
// translate to cells |
|
7351
|
|
|
var segmentCellFirst = cellOffsetToCell(segmentCellOffsetFirst); |
|
7352
|
|
|
var segmentCellLast = cellOffsetToCell(segmentCellOffsetLast); |
|
7353
|
|
|
|
|
7354
|
|
|
// view might be RTL, so order by leftmost column |
|
7355
|
|
|
var cols = [ segmentCellFirst.col, segmentCellLast.col ].sort(); |
|
7356
|
|
|
|
|
7357
|
|
|
// Determine if segment's first/last cell is the beginning/end of the date range. |
|
7358
|
|
|
// We need to compare "day offset" because "cell offsets" are often ambiguous and |
|
7359
|
|
|
// can translate to multiple days, and an edge case reveals itself when we the |
|
7360
|
|
|
// range's first cell is hidden (we don't want isStart to be true). |
|
7361
|
|
|
var isStart = cellOffsetToDayOffset(segmentCellOffsetFirst) == rangeDayOffsetStart; |
|
7362
|
|
|
var isEnd = cellOffsetToDayOffset(segmentCellOffsetLast) + 1 == rangeDayOffsetEnd; |
|
7363
|
|
|
// +1 for comparing exclusively |
|
7364
|
|
|
|
|
7365
|
|
|
segments.push({ |
|
7366
|
|
|
row: row, |
|
7367
|
|
|
leftCol: cols[0], |
|
7368
|
|
|
rightCol: cols[1], |
|
7369
|
|
|
isStart: isStart, |
|
7370
|
|
|
isEnd: isEnd |
|
7371
|
|
|
}); |
|
7372
|
|
|
} |
|
7373
|
|
|
} |
|
7374
|
|
|
|
|
7375
|
|
|
return segments; |
|
7376
|
|
|
} |
|
7377
|
|
|
|
|
7378
|
|
|
|
|
7379
|
|
|
// Returns the date range of the full days the given range visually appears to occupy. |
|
7380
|
|
|
// Returns object with properties `start` (moment) and `end` (moment, exclusive end). |
|
7381
|
|
|
function computeDayRange(start, end) { |
|
7382
|
|
|
var startDay = start.clone().stripTime(); // the beginning of the day the range starts |
|
7383
|
|
|
var endDay; |
|
7384
|
|
|
var endTimeMS; |
|
7385
|
|
|
|
|
7386
|
|
|
if (end) { |
|
7387
|
|
|
endDay = end.clone().stripTime(); // the beginning of the day the range exclusively ends |
|
7388
|
|
|
endTimeMS = +end.time(); // # of milliseconds into `endDay` |
|
7389
|
|
|
|
|
7390
|
|
|
// If the end time is actually inclusively part of the next day and is equal to or |
|
7391
|
|
|
// beyond the next day threshold, adjust the end to be the exclusive end of `endDay`. |
|
7392
|
|
|
// Otherwise, leaving it as inclusive will cause it to exclude `endDay`. |
|
7393
|
|
|
if (endTimeMS && endTimeMS >= nextDayThreshold) { |
|
7394
|
|
|
endDay.add(1, 'days'); |
|
7395
|
|
|
} |
|
7396
|
|
|
} |
|
7397
|
|
|
|
|
7398
|
|
|
// If no end was specified, or if it is within `startDay` but not past nextDayThreshold, |
|
7399
|
|
|
// assign the default duration of one day. |
|
7400
|
|
|
if (!end || endDay <= startDay) { |
|
|
|
|
|
|
7401
|
|
|
endDay = startDay.clone().add(1, 'days'); |
|
7402
|
|
|
} |
|
7403
|
|
|
|
|
7404
|
|
|
return { start: startDay, end: endDay }; |
|
7405
|
|
|
} |
|
7406
|
|
|
|
|
7407
|
|
|
|
|
7408
|
|
|
// Does the given event visually appear to occupy more than one day? |
|
7409
|
|
|
function isMultiDayEvent(event) { |
|
7410
|
|
|
var range = computeDayRange(event.start, event.end); |
|
7411
|
|
|
|
|
7412
|
|
|
return range.end.diff(range.start, 'days') > 1; |
|
7413
|
|
|
} |
|
7414
|
|
|
|
|
7415
|
|
|
} |
|
7416
|
|
|
|
|
7417
|
|
|
;; |
|
7418
|
|
|
|
|
7419
|
|
|
/* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells. |
|
7420
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
|
7421
|
|
|
// It is a manager for a DayGrid subcomponent, which does most of the heavy lifting. |
|
7422
|
|
|
// It is responsible for managing width/height. |
|
7423
|
|
|
|
|
7424
|
|
|
function BasicView(calendar) { |
|
7425
|
|
|
View.call(this, calendar); // call the super-constructor |
|
7426
|
|
|
this.dayGrid = new DayGrid(this); |
|
7427
|
|
|
this.coordMap = this.dayGrid.coordMap; // the view's date-to-cell mapping is identical to the subcomponent's |
|
7428
|
|
|
} |
|
7429
|
|
|
|
|
7430
|
|
|
|
|
7431
|
|
|
BasicView.prototype = createObject(View.prototype); // define the super-class |
|
7432
|
|
|
$.extend(BasicView.prototype, { |
|
7433
|
|
|
|
|
7434
|
|
|
dayGrid: null, // the main subcomponent that does most of the heavy lifting |
|
7435
|
|
|
|
|
7436
|
|
|
dayNumbersVisible: false, // display day numbers on each day cell? |
|
7437
|
|
|
weekNumbersVisible: false, // display week numbers along the side? |
|
7438
|
|
|
|
|
7439
|
|
|
weekNumberWidth: null, // width of all the week-number cells running down the side |
|
7440
|
|
|
|
|
7441
|
|
|
headRowEl: null, // the fake row element of the day-of-week header |
|
7442
|
|
|
|
|
7443
|
|
|
|
|
7444
|
|
|
// Renders the view into `this.el`, which should already be assigned. |
|
7445
|
|
|
// rowCnt, colCnt, and dayNumbersVisible have been calculated by a subclass and passed here. |
|
7446
|
|
|
render: function(rowCnt, colCnt, dayNumbersVisible) { |
|
7447
|
|
|
|
|
7448
|
|
|
// needed for cell-to-date and date-to-cell calculations in View |
|
7449
|
|
|
this.rowCnt = rowCnt; |
|
7450
|
|
|
this.colCnt = colCnt; |
|
7451
|
|
|
|
|
7452
|
|
|
this.dayNumbersVisible = dayNumbersVisible; |
|
7453
|
|
|
this.weekNumbersVisible = this.opt('weekNumbers'); |
|
7454
|
|
|
this.dayGrid.numbersVisible = this.dayNumbersVisible || this.weekNumbersVisible; |
|
7455
|
|
|
|
|
7456
|
|
|
this.el.addClass('fc-basic-view').html(this.renderHtml()); |
|
7457
|
|
|
|
|
7458
|
|
|
this.headRowEl = this.el.find('thead .fc-row'); |
|
7459
|
|
|
|
|
7460
|
|
|
this.scrollerEl = this.el.find('.fc-day-grid-container'); |
|
7461
|
|
|
this.dayGrid.coordMap.containerEl = this.scrollerEl; // constrain clicks/etc to the dimensions of the scroller |
|
7462
|
|
|
|
|
7463
|
|
|
this.dayGrid.el = this.el.find('.fc-day-grid'); |
|
7464
|
|
|
this.dayGrid.render(this.hasRigidRows()); |
|
7465
|
|
|
|
|
7466
|
|
|
View.prototype.render.call(this); // call the super-method |
|
7467
|
|
|
}, |
|
7468
|
|
|
|
|
7469
|
|
|
|
|
7470
|
|
|
// Make subcomponents ready for cleanup |
|
7471
|
|
|
destroy: function() { |
|
7472
|
|
|
this.dayGrid.destroy(); |
|
7473
|
|
|
View.prototype.destroy.call(this); // call the super-method |
|
7474
|
|
|
}, |
|
7475
|
|
|
|
|
7476
|
|
|
|
|
7477
|
|
|
// Builds the HTML skeleton for the view. |
|
7478
|
|
|
// The day-grid component will render inside of a container defined by this HTML. |
|
7479
|
|
|
renderHtml: function() { |
|
7480
|
|
|
return '' + |
|
7481
|
|
|
'<table>' + |
|
7482
|
|
|
'<thead>' + |
|
7483
|
|
|
'<tr>' + |
|
7484
|
|
|
'<td class="' + this.widgetHeaderClass + '">' + |
|
7485
|
|
|
this.dayGrid.headHtml() + // render the day-of-week headers |
|
7486
|
|
|
'</td>' + |
|
7487
|
|
|
'</tr>' + |
|
7488
|
|
|
'</thead>' + |
|
7489
|
|
|
'<tbody>' + |
|
7490
|
|
|
'<tr>' + |
|
7491
|
|
|
'<td class="' + this.widgetContentClass + '">' + |
|
7492
|
|
|
'<div class="fc-day-grid-container">' + |
|
7493
|
|
|
'<div class="fc-day-grid"/>' + |
|
7494
|
|
|
'</div>' + |
|
7495
|
|
|
'</td>' + |
|
7496
|
|
|
'</tr>' + |
|
7497
|
|
|
'</tbody>' + |
|
7498
|
|
|
'</table>'; |
|
7499
|
|
|
}, |
|
7500
|
|
|
|
|
7501
|
|
|
|
|
7502
|
|
|
// Generates the HTML that will go before the day-of week header cells. |
|
7503
|
|
|
// Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL. |
|
7504
|
|
|
headIntroHtml: function() { |
|
7505
|
|
|
if (this.weekNumbersVisible) { |
|
7506
|
|
|
return '' + |
|
7507
|
|
|
'<th class="fc-week-number ' + this.widgetHeaderClass + '" ' + this.weekNumberStyleAttr() + '>' + |
|
7508
|
|
|
'<span>' + // needed for matchCellWidths |
|
7509
|
|
|
htmlEscape(this.opt('weekNumberTitle')) + |
|
7510
|
|
|
'</span>' + |
|
7511
|
|
|
'</th>'; |
|
7512
|
|
|
} |
|
7513
|
|
|
}, |
|
7514
|
|
|
|
|
7515
|
|
|
|
|
7516
|
|
|
// Generates the HTML that will go before content-skeleton cells that display the day/week numbers. |
|
7517
|
|
|
// Queried by the DayGrid subcomponent. Ordering depends on isRTL. |
|
7518
|
|
|
numberIntroHtml: function(row) { |
|
7519
|
|
|
if (this.weekNumbersVisible) { |
|
7520
|
|
|
return '' + |
|
7521
|
|
|
'<td class="fc-week-number" ' + this.weekNumberStyleAttr() + '>' + |
|
7522
|
|
|
'<span>' + // needed for matchCellWidths |
|
7523
|
|
|
this.calendar.calculateWeekNumber(this.cellToDate(row, 0)) + |
|
7524
|
|
|
'</span>' + |
|
7525
|
|
|
'</td>'; |
|
7526
|
|
|
} |
|
7527
|
|
|
}, |
|
7528
|
|
|
|
|
7529
|
|
|
|
|
7530
|
|
|
// Generates the HTML that goes before the day bg cells for each day-row. |
|
7531
|
|
|
// Queried by the DayGrid subcomponent. Ordering depends on isRTL. |
|
7532
|
|
|
dayIntroHtml: function() { |
|
7533
|
|
|
if (this.weekNumbersVisible) { |
|
7534
|
|
|
return '<td class="fc-week-number ' + this.widgetContentClass + '" ' + |
|
7535
|
|
|
this.weekNumberStyleAttr() + '></td>'; |
|
7536
|
|
|
} |
|
7537
|
|
|
}, |
|
7538
|
|
|
|
|
7539
|
|
|
|
|
7540
|
|
|
// Generates the HTML that goes before every other type of row generated by DayGrid. Ordering depends on isRTL. |
|
7541
|
|
|
// Affects helper-skeleton and highlight-skeleton rows. |
|
7542
|
|
|
introHtml: function() { |
|
7543
|
|
|
if (this.weekNumbersVisible) { |
|
7544
|
|
|
return '<td class="fc-week-number" ' + this.weekNumberStyleAttr() + '></td>'; |
|
7545
|
|
|
} |
|
7546
|
|
|
}, |
|
7547
|
|
|
|
|
7548
|
|
|
|
|
7549
|
|
|
// Generates the HTML for the <td>s of the "number" row in the DayGrid's content skeleton. |
|
7550
|
|
|
// The number row will only exist if either day numbers or week numbers are turned on. |
|
7551
|
|
|
numberCellHtml: function(row, col, date) { |
|
7552
|
|
|
var classes; |
|
7553
|
|
|
|
|
7554
|
|
|
if (!this.dayNumbersVisible) { // if there are week numbers but not day numbers |
|
7555
|
|
|
return '<td/>'; // will create an empty space above events :( |
|
7556
|
|
|
} |
|
7557
|
|
|
|
|
7558
|
|
|
classes = this.dayGrid.getDayClasses(date); |
|
7559
|
|
|
classes.unshift('fc-day-number'); |
|
7560
|
|
|
|
|
7561
|
|
|
return '' + |
|
7562
|
|
|
'<td class="' + classes.join(' ') + '" data-date="' + date.format() + '">' + |
|
7563
|
|
|
date.date() + |
|
7564
|
|
|
'</td>'; |
|
7565
|
|
|
}, |
|
7566
|
|
|
|
|
7567
|
|
|
|
|
7568
|
|
|
// Generates an HTML attribute string for setting the width of the week number column, if it is known |
|
7569
|
|
|
weekNumberStyleAttr: function() { |
|
7570
|
|
|
if (this.weekNumberWidth !== null) { |
|
7571
|
|
|
return 'style="width:' + this.weekNumberWidth + 'px"'; |
|
7572
|
|
|
} |
|
7573
|
|
|
return ''; |
|
7574
|
|
|
}, |
|
7575
|
|
|
|
|
7576
|
|
|
|
|
7577
|
|
|
// Determines whether each row should have a constant height |
|
7578
|
|
|
hasRigidRows: function() { |
|
7579
|
|
|
var eventLimit = this.opt('eventLimit'); |
|
7580
|
|
|
return eventLimit && typeof eventLimit !== 'number'; |
|
7581
|
|
|
}, |
|
7582
|
|
|
|
|
7583
|
|
|
|
|
7584
|
|
|
/* Dimensions |
|
7585
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
7586
|
|
|
|
|
7587
|
|
|
|
|
7588
|
|
|
// Refreshes the horizontal dimensions of the view |
|
7589
|
|
|
updateWidth: function() { |
|
7590
|
|
|
if (this.weekNumbersVisible) { |
|
7591
|
|
|
// Make sure all week number cells running down the side have the same width. |
|
7592
|
|
|
// Record the width for cells created later. |
|
7593
|
|
|
this.weekNumberWidth = matchCellWidths( |
|
7594
|
|
|
this.el.find('.fc-week-number') |
|
7595
|
|
|
); |
|
7596
|
|
|
} |
|
7597
|
|
|
}, |
|
7598
|
|
|
|
|
7599
|
|
|
|
|
7600
|
|
|
// Adjusts the vertical dimensions of the view to the specified values |
|
7601
|
|
|
setHeight: function(totalHeight, isAuto) { |
|
7602
|
|
|
var eventLimit = this.opt('eventLimit'); |
|
7603
|
|
|
var scrollerHeight; |
|
7604
|
|
|
|
|
7605
|
|
|
// reset all heights to be natural |
|
7606
|
|
|
unsetScroller(this.scrollerEl); |
|
7607
|
|
|
uncompensateScroll(this.headRowEl); |
|
7608
|
|
|
|
|
7609
|
|
|
this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed |
|
7610
|
|
|
|
|
7611
|
|
|
// is the event limit a constant level number? |
|
7612
|
|
|
if (eventLimit && typeof eventLimit === 'number') { |
|
7613
|
|
|
this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after |
|
7614
|
|
|
} |
|
7615
|
|
|
|
|
7616
|
|
|
scrollerHeight = this.computeScrollerHeight(totalHeight); |
|
7617
|
|
|
this.setGridHeight(scrollerHeight, isAuto); |
|
7618
|
|
|
|
|
7619
|
|
|
// is the event limit dynamically calculated? |
|
7620
|
|
|
if (eventLimit && typeof eventLimit !== 'number') { |
|
7621
|
|
|
this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set |
|
7622
|
|
|
} |
|
7623
|
|
|
|
|
7624
|
|
|
if (!isAuto && setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars? |
|
7625
|
|
|
|
|
7626
|
|
|
compensateScroll(this.headRowEl, getScrollbarWidths(this.scrollerEl)); |
|
7627
|
|
|
|
|
7628
|
|
|
// doing the scrollbar compensation might have created text overflow which created more height. redo |
|
7629
|
|
|
scrollerHeight = this.computeScrollerHeight(totalHeight); |
|
7630
|
|
|
this.scrollerEl.height(scrollerHeight); |
|
7631
|
|
|
|
|
7632
|
|
|
this.restoreScroll(); |
|
7633
|
|
|
} |
|
7634
|
|
|
}, |
|
7635
|
|
|
|
|
7636
|
|
|
|
|
7637
|
|
|
// Sets the height of just the DayGrid component in this view |
|
7638
|
|
|
setGridHeight: function(height, isAuto) { |
|
7639
|
|
|
if (isAuto) { |
|
7640
|
|
|
undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding |
|
7641
|
|
|
} |
|
7642
|
|
|
else { |
|
7643
|
|
|
distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows |
|
7644
|
|
|
} |
|
7645
|
|
|
}, |
|
7646
|
|
|
|
|
7647
|
|
|
|
|
7648
|
|
|
/* Events |
|
7649
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
7650
|
|
|
|
|
7651
|
|
|
|
|
7652
|
|
|
// Renders the given events onto the view and populates the segments array |
|
7653
|
|
|
renderEvents: function(events) { |
|
7654
|
|
|
this.dayGrid.renderEvents(events); |
|
7655
|
|
|
|
|
7656
|
|
|
this.updateHeight(); // must compensate for events that overflow the row |
|
7657
|
|
|
|
|
7658
|
|
|
View.prototype.renderEvents.call(this, events); // call the super-method |
|
7659
|
|
|
}, |
|
7660
|
|
|
|
|
7661
|
|
|
|
|
7662
|
|
|
// Retrieves all segment objects that are rendered in the view |
|
7663
|
|
|
getSegs: function() { |
|
7664
|
|
|
return this.dayGrid.getSegs(); |
|
7665
|
|
|
}, |
|
7666
|
|
|
|
|
7667
|
|
|
|
|
7668
|
|
|
// Unrenders all event elements and clears internal segment data |
|
7669
|
|
|
destroyEvents: function() { |
|
7670
|
|
|
View.prototype.destroyEvents.call(this); // do this before dayGrid's segs have been cleared |
|
7671
|
|
|
|
|
7672
|
|
|
this.recordScroll(); // removing events will reduce height and mess with the scroll, so record beforehand |
|
7673
|
|
|
this.dayGrid.destroyEvents(); |
|
7674
|
|
|
|
|
7675
|
|
|
// we DON'T need to call updateHeight() because: |
|
7676
|
|
|
// A) a renderEvents() call always happens after this, which will eventually call updateHeight() |
|
7677
|
|
|
// B) in IE8, this causes a flash whenever events are rerendered |
|
7678
|
|
|
}, |
|
7679
|
|
|
|
|
7680
|
|
|
|
|
7681
|
|
|
/* Event Dragging |
|
7682
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
7683
|
|
|
|
|
7684
|
|
|
|
|
7685
|
|
|
// Renders a visual indication of an event being dragged over the view. |
|
7686
|
|
|
// A returned value of `true` signals that a mock "helper" event has been rendered. |
|
7687
|
|
|
renderDrag: function(start, end, seg) { |
|
7688
|
|
|
return this.dayGrid.renderDrag(start, end, seg); |
|
7689
|
|
|
}, |
|
7690
|
|
|
|
|
7691
|
|
|
|
|
7692
|
|
|
// Unrenders the visual indication of an event being dragged over the view |
|
7693
|
|
|
destroyDrag: function() { |
|
7694
|
|
|
this.dayGrid.destroyDrag(); |
|
7695
|
|
|
}, |
|
7696
|
|
|
|
|
7697
|
|
|
|
|
7698
|
|
|
/* Selection |
|
7699
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
7700
|
|
|
|
|
7701
|
|
|
|
|
7702
|
|
|
// Renders a visual indication of a selection |
|
7703
|
|
|
renderSelection: function(start, end) { |
|
7704
|
|
|
this.dayGrid.renderSelection(start, end); |
|
7705
|
|
|
}, |
|
7706
|
|
|
|
|
7707
|
|
|
|
|
7708
|
|
|
// Unrenders a visual indications of a selection |
|
7709
|
|
|
destroySelection: function() { |
|
7710
|
|
|
this.dayGrid.destroySelection(); |
|
7711
|
|
|
} |
|
7712
|
|
|
|
|
7713
|
|
|
}); |
|
7714
|
|
|
|
|
7715
|
|
|
;; |
|
7716
|
|
|
|
|
7717
|
|
|
/* A month view with day cells running in rows (one-per-week) and columns |
|
7718
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
|
7719
|
|
|
|
|
7720
|
|
|
setDefaults({ |
|
7721
|
|
|
fixedWeekCount: true |
|
7722
|
|
|
}); |
|
7723
|
|
|
|
|
7724
|
|
|
fcViews.month = MonthView; // register the view |
|
7725
|
|
|
|
|
7726
|
|
|
function MonthView(calendar) { |
|
7727
|
|
|
BasicView.call(this, calendar); // call the super-constructor |
|
7728
|
|
|
} |
|
7729
|
|
|
|
|
7730
|
|
|
|
|
7731
|
|
|
MonthView.prototype = createObject(BasicView.prototype); // define the super-class |
|
7732
|
|
|
$.extend(MonthView.prototype, { |
|
7733
|
|
|
|
|
7734
|
|
|
name: 'month', |
|
7735
|
|
|
|
|
7736
|
|
|
|
|
7737
|
|
|
incrementDate: function(date, delta) { |
|
7738
|
|
|
return date.clone().stripTime().add(delta, 'months').startOf('month'); |
|
7739
|
|
|
}, |
|
7740
|
|
|
|
|
7741
|
|
|
|
|
7742
|
|
|
render: function(date) { |
|
7743
|
|
|
var rowCnt; |
|
7744
|
|
|
|
|
7745
|
|
|
this.intervalStart = date.clone().stripTime().startOf('month'); |
|
7746
|
|
|
this.intervalEnd = this.intervalStart.clone().add(1, 'months'); |
|
7747
|
|
|
|
|
7748
|
|
|
this.start = this.intervalStart.clone(); |
|
7749
|
|
|
this.start = this.skipHiddenDays(this.start); // move past the first week if no visible days |
|
7750
|
|
|
this.start.startOf('week'); |
|
7751
|
|
|
this.start = this.skipHiddenDays(this.start); // move past the first invisible days of the week |
|
7752
|
|
|
|
|
7753
|
|
|
this.end = this.intervalEnd.clone(); |
|
7754
|
|
|
this.end = this.skipHiddenDays(this.end, -1, true); // move in from the last week if no visible days |
|
7755
|
|
|
this.end.add((7 - this.end.weekday()) % 7, 'days'); // move to end of week if not already |
|
7756
|
|
|
this.end = this.skipHiddenDays(this.end, -1, true); // move in from the last invisible days of the week |
|
7757
|
|
|
|
|
7758
|
|
|
rowCnt = Math.ceil( // need to ceil in case there are hidden days |
|
7759
|
|
|
this.end.diff(this.start, 'weeks', true) // returnfloat=true |
|
7760
|
|
|
); |
|
7761
|
|
|
if (this.isFixedWeeks()) { |
|
7762
|
|
|
this.end.add(6 - rowCnt, 'weeks'); |
|
7763
|
|
|
rowCnt = 6; |
|
7764
|
|
|
} |
|
7765
|
|
|
|
|
7766
|
|
|
this.title = this.calendar.formatDate(this.intervalStart, this.opt('titleFormat')); |
|
7767
|
|
|
|
|
7768
|
|
|
BasicView.prototype.render.call(this, rowCnt, this.getCellsPerWeek(), true); // call the super-method |
|
7769
|
|
|
}, |
|
7770
|
|
|
|
|
7771
|
|
|
|
|
7772
|
|
|
// Overrides the default BasicView behavior to have special multi-week auto-height logic |
|
7773
|
|
|
setGridHeight: function(height, isAuto) { |
|
7774
|
|
|
|
|
7775
|
|
|
isAuto = isAuto || this.opt('weekMode') === 'variable'; // LEGACY: weekMode is deprecated |
|
7776
|
|
|
|
|
7777
|
|
|
// if auto, make the height of each row the height that it would be if there were 6 weeks |
|
7778
|
|
|
if (isAuto) { |
|
7779
|
|
|
height *= this.rowCnt / 6; |
|
7780
|
|
|
} |
|
7781
|
|
|
|
|
7782
|
|
|
distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows |
|
7783
|
|
|
}, |
|
7784
|
|
|
|
|
7785
|
|
|
|
|
7786
|
|
|
isFixedWeeks: function() { |
|
7787
|
|
|
var weekMode = this.opt('weekMode'); // LEGACY: weekMode is deprecated |
|
7788
|
|
|
if (weekMode) { |
|
7789
|
|
|
return weekMode === 'fixed'; // if any other type of weekMode, assume NOT fixed |
|
7790
|
|
|
} |
|
7791
|
|
|
|
|
7792
|
|
|
return this.opt('fixedWeekCount'); |
|
7793
|
|
|
} |
|
7794
|
|
|
|
|
7795
|
|
|
}); |
|
7796
|
|
|
|
|
7797
|
|
|
;; |
|
7798
|
|
|
|
|
7799
|
|
|
/* A week view with simple day cells running horizontally |
|
7800
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
|
7801
|
|
|
// TODO: a WeekView mixin for calculating dates and titles |
|
7802
|
|
|
|
|
7803
|
|
|
fcViews.basicWeek = BasicWeekView; // register this view |
|
7804
|
|
|
|
|
7805
|
|
|
function BasicWeekView(calendar) { |
|
7806
|
|
|
BasicView.call(this, calendar); // call the super-constructor |
|
7807
|
|
|
} |
|
7808
|
|
|
|
|
7809
|
|
|
|
|
7810
|
|
|
BasicWeekView.prototype = createObject(BasicView.prototype); // define the super-class |
|
7811
|
|
|
$.extend(BasicWeekView.prototype, { |
|
7812
|
|
|
|
|
7813
|
|
|
name: 'basicWeek', |
|
7814
|
|
|
|
|
7815
|
|
|
|
|
7816
|
|
|
incrementDate: function(date, delta) { |
|
7817
|
|
|
return date.clone().stripTime().add(delta, 'weeks').startOf('week'); |
|
7818
|
|
|
}, |
|
7819
|
|
|
|
|
7820
|
|
|
|
|
7821
|
|
|
render: function(date) { |
|
7822
|
|
|
|
|
7823
|
|
|
this.intervalStart = date.clone().stripTime().startOf('week'); |
|
7824
|
|
|
this.intervalEnd = this.intervalStart.clone().add(1, 'weeks'); |
|
7825
|
|
|
|
|
7826
|
|
|
this.start = this.skipHiddenDays(this.intervalStart); |
|
7827
|
|
|
this.end = this.skipHiddenDays(this.intervalEnd, -1, true); |
|
7828
|
|
|
|
|
7829
|
|
|
this.title = this.calendar.formatRange( |
|
7830
|
|
|
this.start, |
|
7831
|
|
|
this.end.clone().subtract(1), // make inclusive by subtracting 1 ms |
|
7832
|
|
|
this.opt('titleFormat'), |
|
7833
|
|
|
' \u2014 ' // emphasized dash |
|
7834
|
|
|
); |
|
7835
|
|
|
|
|
7836
|
|
|
BasicView.prototype.render.call(this, 1, this.getCellsPerWeek(), false); // call the super-method |
|
7837
|
|
|
} |
|
7838
|
|
|
|
|
7839
|
|
|
}); |
|
7840
|
|
|
;; |
|
7841
|
|
|
|
|
7842
|
|
|
/* A view with a single simple day cell |
|
7843
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
|
7844
|
|
|
|
|
7845
|
|
|
fcViews.basicDay = BasicDayView; // register this view |
|
7846
|
|
|
|
|
7847
|
|
|
function BasicDayView(calendar) { |
|
7848
|
|
|
BasicView.call(this, calendar); // call the super-constructor |
|
7849
|
|
|
} |
|
7850
|
|
|
|
|
7851
|
|
|
|
|
7852
|
|
|
BasicDayView.prototype = createObject(BasicView.prototype); // define the super-class |
|
7853
|
|
|
$.extend(BasicDayView.prototype, { |
|
7854
|
|
|
|
|
7855
|
|
|
name: 'basicDay', |
|
7856
|
|
|
|
|
7857
|
|
|
|
|
7858
|
|
|
incrementDate: function(date, delta) { |
|
7859
|
|
|
var out = date.clone().stripTime().add(delta, 'days'); |
|
7860
|
|
|
out = this.skipHiddenDays(out, delta < 0 ? -1 : 1); |
|
7861
|
|
|
return out; |
|
7862
|
|
|
}, |
|
7863
|
|
|
|
|
7864
|
|
|
|
|
7865
|
|
|
render: function(date) { |
|
7866
|
|
|
|
|
7867
|
|
|
this.start = this.intervalStart = date.clone().stripTime(); |
|
7868
|
|
|
this.end = this.intervalEnd = this.start.clone().add(1, 'days'); |
|
7869
|
|
|
|
|
7870
|
|
|
this.title = this.calendar.formatDate(this.start, this.opt('titleFormat')); |
|
7871
|
|
|
|
|
7872
|
|
|
BasicView.prototype.render.call(this, 1, 1, false); // call the super-method |
|
7873
|
|
|
} |
|
7874
|
|
|
|
|
7875
|
|
|
}); |
|
7876
|
|
|
;; |
|
7877
|
|
|
|
|
7878
|
|
|
/* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically. |
|
7879
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
|
7880
|
|
|
// Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on). |
|
7881
|
|
|
// Responsible for managing width/height. |
|
7882
|
|
|
|
|
7883
|
|
|
setDefaults({ |
|
7884
|
|
|
allDaySlot: true, |
|
7885
|
|
|
allDayText: 'all-day', |
|
7886
|
|
|
|
|
7887
|
|
|
scrollTime: '06:00:00', |
|
7888
|
|
|
|
|
7889
|
|
|
slotDuration: '00:30:00', |
|
7890
|
|
|
|
|
7891
|
|
|
axisFormat: generateAgendaAxisFormat, |
|
7892
|
|
|
timeFormat: { |
|
7893
|
|
|
agenda: generateAgendaTimeFormat |
|
7894
|
|
|
}, |
|
7895
|
|
|
|
|
7896
|
|
|
minTime: '00:00:00', |
|
7897
|
|
|
maxTime: '24:00:00', |
|
7898
|
|
|
slotEventOverlap: true |
|
7899
|
|
|
}); |
|
7900
|
|
|
|
|
7901
|
|
|
var AGENDA_ALL_DAY_EVENT_LIMIT = 5; |
|
7902
|
|
|
|
|
7903
|
|
|
|
|
7904
|
|
|
function generateAgendaAxisFormat(options, langData) { |
|
7905
|
|
|
return langData.longDateFormat('LT') |
|
7906
|
|
|
.replace(':mm', '(:mm)') |
|
7907
|
|
|
.replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs |
|
7908
|
|
|
.replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand |
|
7909
|
|
|
} |
|
7910
|
|
|
|
|
7911
|
|
|
|
|
7912
|
|
|
function generateAgendaTimeFormat(options, langData) { |
|
7913
|
|
|
return langData.longDateFormat('LT') |
|
7914
|
|
|
.replace(/\s*a$/i, ''); // remove trailing AM/PM |
|
7915
|
|
|
} |
|
7916
|
|
|
|
|
7917
|
|
|
|
|
7918
|
|
|
function AgendaView(calendar) { |
|
7919
|
|
|
View.call(this, calendar); // call the super-constructor |
|
7920
|
|
|
|
|
7921
|
|
|
this.timeGrid = new TimeGrid(this); |
|
7922
|
|
|
|
|
7923
|
|
|
if (this.opt('allDaySlot')) { // should we display the "all-day" area? |
|
7924
|
|
|
this.dayGrid = new DayGrid(this); // the all-day subcomponent of this view |
|
7925
|
|
|
|
|
7926
|
|
|
// the coordinate grid will be a combination of both subcomponents' grids |
|
7927
|
|
|
this.coordMap = new ComboCoordMap([ |
|
7928
|
|
|
this.dayGrid.coordMap, |
|
7929
|
|
|
this.timeGrid.coordMap |
|
7930
|
|
|
]); |
|
7931
|
|
|
} |
|
7932
|
|
|
else { |
|
7933
|
|
|
this.coordMap = this.timeGrid.coordMap; |
|
7934
|
|
|
} |
|
7935
|
|
|
} |
|
7936
|
|
|
|
|
7937
|
|
|
|
|
7938
|
|
|
AgendaView.prototype = createObject(View.prototype); // define the super-class |
|
7939
|
|
|
$.extend(AgendaView.prototype, { |
|
7940
|
|
|
|
|
7941
|
|
|
timeGrid: null, // the main time-grid subcomponent of this view |
|
7942
|
|
|
dayGrid: null, // the "all-day" subcomponent. if all-day is turned off, this will be null |
|
7943
|
|
|
|
|
7944
|
|
|
axisWidth: null, // the width of the time axis running down the side |
|
7945
|
|
|
|
|
7946
|
|
|
noScrollRowEls: null, // set of fake row elements that must compensate when scrollerEl has scrollbars |
|
7947
|
|
|
|
|
7948
|
|
|
// when the time-grid isn't tall enough to occupy the given height, we render an <hr> underneath |
|
7949
|
|
|
bottomRuleEl: null, |
|
7950
|
|
|
bottomRuleHeight: null, |
|
7951
|
|
|
|
|
7952
|
|
|
|
|
7953
|
|
|
/* Rendering |
|
7954
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
7955
|
|
|
|
|
7956
|
|
|
|
|
7957
|
|
|
// Renders the view into `this.el`, which has already been assigned. |
|
7958
|
|
|
// `colCnt` has been calculated by a subclass and passed here. |
|
7959
|
|
|
render: function(colCnt) { |
|
7960
|
|
|
|
|
7961
|
|
|
// needed for cell-to-date and date-to-cell calculations in View |
|
7962
|
|
|
this.rowCnt = 1; |
|
7963
|
|
|
this.colCnt = colCnt; |
|
7964
|
|
|
|
|
7965
|
|
|
this.el.addClass('fc-agenda-view').html(this.renderHtml()); |
|
7966
|
|
|
|
|
7967
|
|
|
// the element that wraps the time-grid that will probably scroll |
|
7968
|
|
|
this.scrollerEl = this.el.find('.fc-time-grid-container'); |
|
7969
|
|
|
this.timeGrid.coordMap.containerEl = this.scrollerEl; // don't accept clicks/etc outside of this |
|
7970
|
|
|
|
|
7971
|
|
|
this.timeGrid.el = this.el.find('.fc-time-grid'); |
|
7972
|
|
|
this.timeGrid.render(); |
|
7973
|
|
|
|
|
7974
|
|
|
// the <hr> that sometimes displays under the time-grid |
|
7975
|
|
|
this.bottomRuleEl = $('<hr class="' + this.widgetHeaderClass + '"/>') |
|
7976
|
|
|
.appendTo(this.timeGrid.el); // inject it into the time-grid |
|
7977
|
|
|
|
|
7978
|
|
|
if (this.dayGrid) { |
|
7979
|
|
|
this.dayGrid.el = this.el.find('.fc-day-grid'); |
|
7980
|
|
|
this.dayGrid.render(); |
|
7981
|
|
|
|
|
7982
|
|
|
// have the day-grid extend it's coordinate area over the <hr> dividing the two grids |
|
7983
|
|
|
this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight(); |
|
7984
|
|
|
} |
|
7985
|
|
|
|
|
7986
|
|
|
this.noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller |
|
7987
|
|
|
|
|
7988
|
|
|
View.prototype.render.call(this); // call the super-method |
|
7989
|
|
|
|
|
7990
|
|
|
this.resetScroll(); // do this after sizes have been set |
|
7991
|
|
|
}, |
|
7992
|
|
|
|
|
7993
|
|
|
|
|
7994
|
|
|
// Make subcomponents ready for cleanup |
|
7995
|
|
|
destroy: function() { |
|
7996
|
|
|
this.timeGrid.destroy(); |
|
7997
|
|
|
if (this.dayGrid) { |
|
7998
|
|
|
this.dayGrid.destroy(); |
|
7999
|
|
|
} |
|
8000
|
|
|
View.prototype.destroy.call(this); // call the super-method |
|
8001
|
|
|
}, |
|
8002
|
|
|
|
|
8003
|
|
|
|
|
8004
|
|
|
// Builds the HTML skeleton for the view. |
|
8005
|
|
|
// The day-grid and time-grid components will render inside containers defined by this HTML. |
|
8006
|
|
|
renderHtml: function() { |
|
8007
|
|
|
return '' + |
|
8008
|
|
|
'<table>' + |
|
8009
|
|
|
'<thead>' + |
|
8010
|
|
|
'<tr>' + |
|
8011
|
|
|
'<td class="' + this.widgetHeaderClass + '">' + |
|
8012
|
|
|
this.timeGrid.headHtml() + // render the day-of-week headers |
|
8013
|
|
|
'</td>' + |
|
8014
|
|
|
'</tr>' + |
|
8015
|
|
|
'</thead>' + |
|
8016
|
|
|
'<tbody>' + |
|
8017
|
|
|
'<tr>' + |
|
8018
|
|
|
'<td class="' + this.widgetContentClass + '">' + |
|
8019
|
|
|
(this.dayGrid ? |
|
8020
|
|
|
'<div class="fc-day-grid"/>' + |
|
8021
|
|
|
'<hr class="' + this.widgetHeaderClass + '"/>' : |
|
8022
|
|
|
'' |
|
8023
|
|
|
) + |
|
8024
|
|
|
'<div class="fc-time-grid-container">' + |
|
8025
|
|
|
'<div class="fc-time-grid"/>' + |
|
8026
|
|
|
'</div>' + |
|
8027
|
|
|
'</td>' + |
|
8028
|
|
|
'</tr>' + |
|
8029
|
|
|
'</tbody>' + |
|
8030
|
|
|
'</table>'; |
|
8031
|
|
|
}, |
|
8032
|
|
|
|
|
8033
|
|
|
|
|
8034
|
|
|
// Generates the HTML that will go before the day-of week header cells. |
|
8035
|
|
|
// Queried by the TimeGrid subcomponent when generating rows. Ordering depends on isRTL. |
|
8036
|
|
|
headIntroHtml: function() { |
|
8037
|
|
|
var date; |
|
8038
|
|
|
var weekNumber; |
|
8039
|
|
|
var weekTitle; |
|
8040
|
|
|
var weekText; |
|
8041
|
|
|
|
|
8042
|
|
|
if (this.opt('weekNumbers')) { |
|
8043
|
|
|
date = this.cellToDate(0, 0); |
|
8044
|
|
|
weekNumber = this.calendar.calculateWeekNumber(date); |
|
8045
|
|
|
weekTitle = this.opt('weekNumberTitle'); |
|
8046
|
|
|
|
|
8047
|
|
|
if (this.opt('isRTL')) { |
|
8048
|
|
|
weekText = weekNumber + weekTitle; |
|
8049
|
|
|
} |
|
8050
|
|
|
else { |
|
8051
|
|
|
weekText = weekTitle + weekNumber; |
|
8052
|
|
|
} |
|
8053
|
|
|
|
|
8054
|
|
|
return '' + |
|
8055
|
|
|
'<th class="fc-axis fc-week-number ' + this.widgetHeaderClass + '" ' + this.axisStyleAttr() + '>' + |
|
8056
|
|
|
'<span>' + // needed for matchCellWidths |
|
8057
|
|
|
htmlEscape(weekText) + |
|
8058
|
|
|
'</span>' + |
|
8059
|
|
|
'</th>'; |
|
8060
|
|
|
} |
|
8061
|
|
|
else { |
|
8062
|
|
|
return '<th class="fc-axis ' + this.widgetHeaderClass + '" ' + this.axisStyleAttr() + '></th>'; |
|
8063
|
|
|
} |
|
8064
|
|
|
}, |
|
8065
|
|
|
|
|
8066
|
|
|
|
|
8067
|
|
|
// Generates the HTML that goes before the all-day cells. |
|
8068
|
|
|
// Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL. |
|
8069
|
|
|
dayIntroHtml: function() { |
|
8070
|
|
|
return '' + |
|
8071
|
|
|
'<td class="fc-axis ' + this.widgetContentClass + '" ' + this.axisStyleAttr() + '>' + |
|
8072
|
|
|
'<span>' + // needed for matchCellWidths |
|
8073
|
|
|
(this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'))) + |
|
8074
|
|
|
'</span>' + |
|
8075
|
|
|
'</td>'; |
|
8076
|
|
|
}, |
|
8077
|
|
|
|
|
8078
|
|
|
|
|
8079
|
|
|
// Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column. |
|
8080
|
|
|
slotBgIntroHtml: function() { |
|
8081
|
|
|
return '<td class="fc-axis ' + this.widgetContentClass + '" ' + this.axisStyleAttr() + '></td>'; |
|
8082
|
|
|
}, |
|
8083
|
|
|
|
|
8084
|
|
|
|
|
8085
|
|
|
// Generates the HTML that goes before all other types of cells. |
|
8086
|
|
|
// Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid. |
|
8087
|
|
|
// Queried by the TimeGrid and DayGrid subcomponents when generating rows. Ordering depends on isRTL. |
|
8088
|
|
|
introHtml: function() { |
|
8089
|
|
|
return '<td class="fc-axis" ' + this.axisStyleAttr() + '></td>'; |
|
8090
|
|
|
}, |
|
8091
|
|
|
|
|
8092
|
|
|
|
|
8093
|
|
|
// Generates an HTML attribute string for setting the width of the axis, if it is known |
|
8094
|
|
|
axisStyleAttr: function() { |
|
8095
|
|
|
if (this.axisWidth !== null) { |
|
8096
|
|
|
return 'style="width:' + this.axisWidth + 'px"'; |
|
8097
|
|
|
} |
|
8098
|
|
|
return ''; |
|
8099
|
|
|
}, |
|
8100
|
|
|
|
|
8101
|
|
|
|
|
8102
|
|
|
/* Dimensions |
|
8103
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
8104
|
|
|
|
|
8105
|
|
|
updateSize: function(isResize) { |
|
8106
|
|
|
if (isResize) { |
|
8107
|
|
|
this.timeGrid.resize(); |
|
8108
|
|
|
} |
|
8109
|
|
|
View.prototype.updateSize.call(this, isResize); |
|
8110
|
|
|
}, |
|
8111
|
|
|
|
|
8112
|
|
|
|
|
8113
|
|
|
// Refreshes the horizontal dimensions of the view |
|
8114
|
|
|
updateWidth: function() { |
|
8115
|
|
|
// make all axis cells line up, and record the width so newly created axis cells will have it |
|
8116
|
|
|
this.axisWidth = matchCellWidths(this.el.find('.fc-axis')); |
|
8117
|
|
|
}, |
|
8118
|
|
|
|
|
8119
|
|
|
|
|
8120
|
|
|
// Adjusts the vertical dimensions of the view to the specified values |
|
8121
|
|
|
setHeight: function(totalHeight, isAuto) { |
|
8122
|
|
|
var eventLimit; |
|
8123
|
|
|
var scrollerHeight; |
|
8124
|
|
|
|
|
8125
|
|
|
if (this.bottomRuleHeight === null) { |
|
8126
|
|
|
// calculate the height of the rule the very first time |
|
8127
|
|
|
this.bottomRuleHeight = this.bottomRuleEl.outerHeight(); |
|
8128
|
|
|
} |
|
8129
|
|
|
this.bottomRuleEl.hide(); // .show() will be called later if this <hr> is necessary |
|
8130
|
|
|
|
|
8131
|
|
|
// reset all dimensions back to the original state |
|
8132
|
|
|
this.scrollerEl.css('overflow', ''); |
|
8133
|
|
|
unsetScroller(this.scrollerEl); |
|
8134
|
|
|
uncompensateScroll(this.noScrollRowEls); |
|
8135
|
|
|
|
|
8136
|
|
|
// limit number of events in the all-day area |
|
8137
|
|
|
if (this.dayGrid) { |
|
8138
|
|
|
this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed |
|
8139
|
|
|
|
|
8140
|
|
|
eventLimit = this.opt('eventLimit'); |
|
8141
|
|
|
if (eventLimit && typeof eventLimit !== 'number') { |
|
8142
|
|
|
eventLimit = AGENDA_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number |
|
8143
|
|
|
} |
|
8144
|
|
|
if (eventLimit) { |
|
8145
|
|
|
this.dayGrid.limitRows(eventLimit); |
|
8146
|
|
|
} |
|
8147
|
|
|
} |
|
8148
|
|
|
|
|
8149
|
|
|
if (!isAuto) { // should we force dimensions of the scroll container, or let the contents be natural height? |
|
8150
|
|
|
|
|
8151
|
|
|
scrollerHeight = this.computeScrollerHeight(totalHeight); |
|
8152
|
|
|
if (setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars? |
|
8153
|
|
|
|
|
8154
|
|
|
// make the all-day and header rows lines up |
|
8155
|
|
|
compensateScroll(this.noScrollRowEls, getScrollbarWidths(this.scrollerEl)); |
|
8156
|
|
|
|
|
8157
|
|
|
// the scrollbar compensation might have changed text flow, which might affect height, so recalculate |
|
8158
|
|
|
// and reapply the desired height to the scroller. |
|
8159
|
|
|
scrollerHeight = this.computeScrollerHeight(totalHeight); |
|
8160
|
|
|
this.scrollerEl.height(scrollerHeight); |
|
8161
|
|
|
|
|
8162
|
|
|
this.restoreScroll(); |
|
8163
|
|
|
} |
|
8164
|
|
|
else { // no scrollbars |
|
8165
|
|
|
// still, force a height and display the bottom rule (marks the end of day) |
|
8166
|
|
|
this.scrollerEl.height(scrollerHeight).css('overflow', 'hidden'); // in case <hr> goes outside |
|
8167
|
|
|
this.bottomRuleEl.show(); |
|
8168
|
|
|
} |
|
8169
|
|
|
} |
|
8170
|
|
|
}, |
|
8171
|
|
|
|
|
8172
|
|
|
|
|
8173
|
|
|
// Sets the scroll value of the scroller to the intial pre-configured state prior to allowing the user to change it. |
|
8174
|
|
|
resetScroll: function() { |
|
8175
|
|
|
var _this = this; |
|
8176
|
|
|
var scrollTime = moment.duration(this.opt('scrollTime')); |
|
8177
|
|
|
var top = this.timeGrid.computeTimeTop(scrollTime); |
|
8178
|
|
|
|
|
8179
|
|
|
// zoom can give weird floating-point values. rather scroll a little bit further |
|
8180
|
|
|
top = Math.ceil(top); |
|
8181
|
|
|
|
|
8182
|
|
|
if (top) { |
|
8183
|
|
|
top++; // to overcome top border that slots beyond the first have. looks better |
|
8184
|
|
|
} |
|
8185
|
|
|
|
|
8186
|
|
|
function scroll() { |
|
8187
|
|
|
_this.scrollerEl.scrollTop(top); |
|
8188
|
|
|
} |
|
8189
|
|
|
|
|
8190
|
|
|
scroll(); |
|
8191
|
|
|
setTimeout(scroll, 0); // overrides any previous scroll state made by the browser |
|
8192
|
|
|
}, |
|
8193
|
|
|
|
|
8194
|
|
|
|
|
8195
|
|
|
/* Events |
|
8196
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
8197
|
|
|
|
|
8198
|
|
|
|
|
8199
|
|
|
// Renders events onto the view and populates the View's segment array |
|
8200
|
|
|
renderEvents: function(events) { |
|
8201
|
|
|
var dayEvents = []; |
|
8202
|
|
|
var timedEvents = []; |
|
8203
|
|
|
var daySegs = []; |
|
|
|
|
|
|
8204
|
|
|
var timedSegs; |
|
8205
|
|
|
var i; |
|
8206
|
|
|
|
|
8207
|
|
|
// separate the events into all-day and timed |
|
8208
|
|
|
for (i = 0; i < events.length; i++) { |
|
8209
|
|
|
if (events[i].allDay) { |
|
8210
|
|
|
dayEvents.push(events[i]); |
|
8211
|
|
|
} |
|
8212
|
|
|
else { |
|
8213
|
|
|
timedEvents.push(events[i]); |
|
8214
|
|
|
} |
|
8215
|
|
|
} |
|
8216
|
|
|
|
|
8217
|
|
|
// render the events in the subcomponents |
|
8218
|
|
|
timedSegs = this.timeGrid.renderEvents(timedEvents); |
|
|
|
|
|
|
8219
|
|
|
if (this.dayGrid) { |
|
8220
|
|
|
daySegs = this.dayGrid.renderEvents(dayEvents); |
|
8221
|
|
|
} |
|
8222
|
|
|
|
|
8223
|
|
|
// the all-day area is flexible and might have a lot of events, so shift the height |
|
8224
|
|
|
this.updateHeight(); |
|
8225
|
|
|
|
|
8226
|
|
|
View.prototype.renderEvents.call(this, events); // call the super-method |
|
8227
|
|
|
}, |
|
8228
|
|
|
|
|
8229
|
|
|
|
|
8230
|
|
|
// Retrieves all segment objects that are rendered in the view |
|
8231
|
|
|
getSegs: function() { |
|
8232
|
|
|
return this.timeGrid.getSegs().concat( |
|
8233
|
|
|
this.dayGrid ? this.dayGrid.getSegs() : [] |
|
8234
|
|
|
); |
|
8235
|
|
|
}, |
|
8236
|
|
|
|
|
8237
|
|
|
|
|
8238
|
|
|
// Unrenders all event elements and clears internal segment data |
|
8239
|
|
|
destroyEvents: function() { |
|
8240
|
|
|
View.prototype.destroyEvents.call(this); // do this before the grids' segs have been cleared |
|
8241
|
|
|
|
|
8242
|
|
|
// if destroyEvents is being called as part of an event rerender, renderEvents will be called shortly |
|
8243
|
|
|
// after, so remember what the scroll value was so we can restore it. |
|
8244
|
|
|
this.recordScroll(); |
|
8245
|
|
|
|
|
8246
|
|
|
// destroy the events in the subcomponents |
|
8247
|
|
|
this.timeGrid.destroyEvents(); |
|
8248
|
|
|
if (this.dayGrid) { |
|
8249
|
|
|
this.dayGrid.destroyEvents(); |
|
8250
|
|
|
} |
|
8251
|
|
|
|
|
8252
|
|
|
// we DON'T need to call updateHeight() because: |
|
8253
|
|
|
// A) a renderEvents() call always happens after this, which will eventually call updateHeight() |
|
8254
|
|
|
// B) in IE8, this causes a flash whenever events are rerendered |
|
8255
|
|
|
}, |
|
8256
|
|
|
|
|
8257
|
|
|
|
|
8258
|
|
|
/* Event Dragging |
|
8259
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
8260
|
|
|
|
|
8261
|
|
|
|
|
8262
|
|
|
// Renders a visual indication of an event being dragged over the view. |
|
8263
|
|
|
// A returned value of `true` signals that a mock "helper" event has been rendered. |
|
8264
|
|
|
renderDrag: function(start, end, seg) { |
|
8265
|
|
|
if (start.hasTime()) { |
|
8266
|
|
|
return this.timeGrid.renderDrag(start, end, seg); |
|
8267
|
|
|
} |
|
8268
|
|
|
else if (this.dayGrid) { |
|
8269
|
|
|
return this.dayGrid.renderDrag(start, end, seg); |
|
8270
|
|
|
} |
|
8271
|
|
|
}, |
|
8272
|
|
|
|
|
8273
|
|
|
|
|
8274
|
|
|
// Unrenders a visual indications of an event being dragged over the view |
|
8275
|
|
|
destroyDrag: function() { |
|
8276
|
|
|
this.timeGrid.destroyDrag(); |
|
8277
|
|
|
if (this.dayGrid) { |
|
8278
|
|
|
this.dayGrid.destroyDrag(); |
|
8279
|
|
|
} |
|
8280
|
|
|
}, |
|
8281
|
|
|
|
|
8282
|
|
|
|
|
8283
|
|
|
/* Selection |
|
8284
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
|
8285
|
|
|
|
|
8286
|
|
|
|
|
8287
|
|
|
// Renders a visual indication of a selection |
|
8288
|
|
|
renderSelection: function(start, end) { |
|
8289
|
|
|
if (start.hasTime() || end.hasTime()) { |
|
8290
|
|
|
this.timeGrid.renderSelection(start, end); |
|
8291
|
|
|
} |
|
8292
|
|
|
else if (this.dayGrid) { |
|
8293
|
|
|
this.dayGrid.renderSelection(start, end); |
|
8294
|
|
|
} |
|
8295
|
|
|
}, |
|
8296
|
|
|
|
|
8297
|
|
|
|
|
8298
|
|
|
// Unrenders a visual indications of a selection |
|
8299
|
|
|
destroySelection: function() { |
|
8300
|
|
|
this.timeGrid.destroySelection(); |
|
8301
|
|
|
if (this.dayGrid) { |
|
8302
|
|
|
this.dayGrid.destroySelection(); |
|
8303
|
|
|
} |
|
8304
|
|
|
} |
|
8305
|
|
|
|
|
8306
|
|
|
}); |
|
8307
|
|
|
|
|
8308
|
|
|
;; |
|
8309
|
|
|
|
|
8310
|
|
|
/* A week view with an all-day cell area at the top, and a time grid below |
|
8311
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
|
8312
|
|
|
// TODO: a WeekView mixin for calculating dates and titles |
|
8313
|
|
|
|
|
8314
|
|
|
fcViews.agendaWeek = AgendaWeekView; // register the view |
|
8315
|
|
|
|
|
8316
|
|
|
function AgendaWeekView(calendar) { |
|
8317
|
|
|
AgendaView.call(this, calendar); // call the super-constructor |
|
8318
|
|
|
} |
|
8319
|
|
|
|
|
8320
|
|
|
|
|
8321
|
|
|
AgendaWeekView.prototype = createObject(AgendaView.prototype); // define the super-class |
|
8322
|
|
|
$.extend(AgendaWeekView.prototype, { |
|
8323
|
|
|
|
|
8324
|
|
|
name: 'agendaWeek', |
|
8325
|
|
|
|
|
8326
|
|
|
|
|
8327
|
|
|
incrementDate: function(date, delta) { |
|
8328
|
|
|
return date.clone().stripTime().add(delta, 'weeks').startOf('week'); |
|
8329
|
|
|
}, |
|
8330
|
|
|
|
|
8331
|
|
|
|
|
8332
|
|
|
render: function(date) { |
|
8333
|
|
|
|
|
8334
|
|
|
this.intervalStart = date.clone().stripTime().startOf('week'); |
|
8335
|
|
|
this.intervalEnd = this.intervalStart.clone().add(1, 'weeks'); |
|
8336
|
|
|
|
|
8337
|
|
|
this.start = this.skipHiddenDays(this.intervalStart); |
|
8338
|
|
|
this.end = this.skipHiddenDays(this.intervalEnd, -1, true); |
|
8339
|
|
|
|
|
8340
|
|
|
this.title = this.calendar.formatRange( |
|
8341
|
|
|
this.start, |
|
8342
|
|
|
this.end.clone().subtract(1), // make inclusive by subtracting 1 ms |
|
8343
|
|
|
this.opt('titleFormat'), |
|
8344
|
|
|
' \u2014 ' // emphasized dash |
|
8345
|
|
|
); |
|
8346
|
|
|
|
|
8347
|
|
|
AgendaView.prototype.render.call(this, this.getCellsPerWeek()); // call the super-method |
|
8348
|
|
|
} |
|
8349
|
|
|
|
|
8350
|
|
|
}); |
|
8351
|
|
|
|
|
8352
|
|
|
;; |
|
8353
|
|
|
|
|
8354
|
|
|
/* A day view with an all-day cell area at the top, and a time grid below |
|
8355
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
|
8356
|
|
|
|
|
8357
|
|
|
fcViews.agendaDay = AgendaDayView; // register the view |
|
8358
|
|
|
|
|
8359
|
|
|
function AgendaDayView(calendar) { |
|
8360
|
|
|
AgendaView.call(this, calendar); // call the super-constructor |
|
8361
|
|
|
} |
|
8362
|
|
|
|
|
8363
|
|
|
|
|
8364
|
|
|
AgendaDayView.prototype = createObject(AgendaView.prototype); // define the super-class |
|
8365
|
|
|
$.extend(AgendaDayView.prototype, { |
|
8366
|
|
|
|
|
8367
|
|
|
name: 'agendaDay', |
|
8368
|
|
|
|
|
8369
|
|
|
|
|
8370
|
|
|
incrementDate: function(date, delta) { |
|
8371
|
|
|
var out = date.clone().stripTime().add(delta, 'days'); |
|
8372
|
|
|
out = this.skipHiddenDays(out, delta < 0 ? -1 : 1); |
|
8373
|
|
|
return out; |
|
8374
|
|
|
}, |
|
8375
|
|
|
|
|
8376
|
|
|
|
|
8377
|
|
|
render: function(date) { |
|
8378
|
|
|
|
|
8379
|
|
|
this.start = this.intervalStart = date.clone().stripTime(); |
|
8380
|
|
|
this.end = this.intervalEnd = this.start.clone().add(1, 'days'); |
|
8381
|
|
|
|
|
8382
|
|
|
this.title = this.calendar.formatDate(this.start, this.opt('titleFormat')); |
|
8383
|
|
|
|
|
8384
|
|
|
AgendaView.prototype.render.call(this, 1); // call the super-method |
|
8385
|
|
|
} |
|
8386
|
|
|
|
|
8387
|
|
|
}); |
|
8388
|
|
|
|
|
8389
|
|
|
;; |
|
8390
|
|
|
|
|
8391
|
|
|
}); |